進(jìn)程標(biāo)識(shí)
在介紹進(jìn)程的創(chuàng)建、啟動(dòng)與終止之前,首先了解一下進(jìn)程的唯一標(biāo)識(shí)——進(jìn)程 ID,它是一個(gè)非負(fù)整數(shù),在系統(tǒng)范圍內(nèi)唯一,不過(guò)這種唯一是相對(duì)的,當(dāng)一個(gè)進(jìn)程消亡后,它的 ID 可能被重用。不過(guò)大多數(shù) Unix 系統(tǒng)實(shí)現(xiàn)延遲重用算法,防止將新進(jìn)程誤認(rèn)為是使用同一 ID 的某個(gè)已終止的進(jìn)程,下面這個(gè)例子展示了這一點(diǎn):
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <set>
int main (int argc, char *argv[])
{
std::set<pid_t> pids;
pid_t pid = getpid();
time_t start = time(NULL);
pids.insert(pid);
while (true)
{
if ((pid = fork ()) < 0)
{
printf ("fork error\n");
return 1;
}
else if (pid == 0)
{
printf ("[%u] child running\n", getpid());
break;
}
else
{
printf ("fork and exec child %u\n", pid);
int status = 0;
pid = wait(&status);
printf ("wait child %u return %d\n", pid, status);
if (pids.find (pid) == pids.end())
{
pids.insert(pid);
}
else
{
time_t end = time(NULL);
printf ("duplicated pid find: %u, total %lu, elapse %lu\n", pid, pids.size(), end-start);
break;
}
}
}
exit (0);
}
上面的程序制造了一個(gè)進(jìn)程 ID 復(fù)用的場(chǎng)景:父進(jìn)程不斷創(chuàng)建子進(jìn)程,將它的進(jìn)程 ID 保存在 set 容器中,并將每次新創(chuàng)建的 pid 與容器中已有的進(jìn)行對(duì)比,如果發(fā)現(xiàn)有重復(fù)的 pid,則打印一條消息退出循環(huán),下面是程序輸出日志:
> ./pid
fork and exec child 18687
[18687] child running
wait child 18687 return 0
fork and exec child 18688
[18688] child running
wait child 18688 return 0
fork and exec child 18689
...
wait child 18683 return 0
fork and exec child 18684
[18684] child running
wait child 18684 return 0
fork and exec child 18685
[18685] child running
wait child 18685 return 0
fork and exec child 18687
[18687] child running
wait child 18687 return 0
duplicated pid find: 18687, total 31930, elapse 8
在大約創(chuàng)建了 3W 個(gè)進(jìn)程后,進(jìn)程 ID 終于復(fù)用了,整個(gè)耗時(shí)大約 8 秒左右,可見(jiàn)在頻繁創(chuàng)建進(jìn)程的場(chǎng)景中,進(jìn)程 ID 被復(fù)用的間隔還是很快的,如果依賴進(jìn)程 ID 的唯一性做一些記錄的話,還是要小心,例如使用進(jìn)程 ID 做為文件名,最好是加上時(shí)間戳等其它維度以確保唯一性。
另外一個(gè)有趣的現(xiàn)象是,進(jìn)程 ID 重復(fù)時(shí),剛好是第一個(gè)子進(jìn)程的進(jìn)程 ID,看起來(lái)這個(gè)進(jìn)程 ID 分配是個(gè)周而復(fù)始的過(guò)程,在漲到一定數(shù)量后會(huì)回卷,追蹤中間的日志,發(fā)現(xiàn)有以下輸出:
...
[32765] child running
wait child 32765 return 0
fork and exec child 32766
[32766] child running
wait child 32766 return 0
fork and exec child 32767
[32767] child running
wait child 32767 return 0
fork and exec child 300
[300] child running
wait child 300 return 0
fork and exec child 313
[313] child running
wait child 313 return 0
fork and exec child 314
[314] child running
wait child 314 return 0
...
看起來(lái)最大達(dá)到 32767 (SHORT_MAX) 后就開(kāi)始回卷了,這比我想象的要早,畢竟 pid_t 類型為 4 字節(jié)整型:
sizeof (pid_t) = 4
最大可以達(dá)到 2147483647,這也許是出于某種向后兼容考慮吧。在 macOS 上這個(gè)過(guò)程略長(zhǎng)一些:
> ./pid
fork and exec child 12629
[12629] child running
wait child 12629 return 0
fork and exec child 12630
[12630] child running
wait child 12630 return 0
fork and exec child 12631
[12631] child running
wait child 12631 return 0
...
[12626] child running
wait child 12626 return 0
fork and exec child 12627
[12627] child running
wait child 12627 return 0
fork and exec child 12629
[12629] child running
wait child 12629 return 0
duplicated pid find: 12629, total 99420, elapse 69
總共產(chǎn)生了不到 10W 個(gè) pid,歷時(shí)一分多鐘,看起來(lái)要比 linux 做的好一點(diǎn)。查看中間日志,pid 也發(fā)生了回卷:
...
fork and exec child 99996
[99996] child running
wait child 99996 return 0
fork and exec child 99997
[99997] child running
wait child 99997 return 0
fork and exec child 99998
[99998] child running
wait child 99998 return 0
fork and exec child 100
[100] child running
wait child 100 return 0
fork and exec child 102
[102] child running
wait child 102 return 0
fork and exec child 103
[103] child running
wait child 103 return 0
...
回卷點(diǎn)是 99999,emmmm 有意思,會(huì)不會(huì)是喬布斯定的,哈哈。
雖然進(jìn)程 ID 的合法范圍是 [0~INT_MAX],但實(shí)際上前幾個(gè)進(jìn)程 ID 會(huì)被系統(tǒng)占用:
- 0: swapper 進(jìn)程 (調(diào)度)
- 1: init 進(jìn)程 (用戶態(tài))
- …
其中 ID=0 的通常是調(diào)度進(jìn)程,也稱為交換進(jìn)程,是內(nèi)核的一部分,并不執(zhí)行任何磁盤上的程序,因此也被稱為系統(tǒng)進(jìn)程;ID=1 的通常是 init 進(jìn)程,在自舉過(guò)程結(jié)束時(shí)由內(nèi)核調(diào)用,該程序的程序文件在 UNIX 早期版本中是 /sbin/init,不過(guò)在我的測(cè)試機(jī) CentOS 上是 /usr/lib/systemd/systemd:
> ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Oct24 ? 00:00:19 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
root 2 0 0 Oct24 ? 00:00:00 [kthreadd]
root 4 2 0 Oct24 ? 00:00:00 [kworker/0:0H]
root 6 2 0 Oct24 ? 00:00:01 [ksoftirqd/0]
root 7 2 0 Oct24 ? 00:00:01 [migration/0]
root 8 2 0 Oct24 ? 00:00:00 [rcu_bh]
...
查看文件系統(tǒng):
> ls -lh /sbin/init
lrwxrwxrwx. 1 root root 22 Sep 7 2022 /sbin/init -> ../lib/systemd/systemd
就是個(gè)軟鏈接,其實(shí)是一回事。在 macOS 又略有不同,
> ps -ef
UID PID PPID C STIME TTY TIME CMD
0 1 0 0 3:34PM ?? 0:15.45 /sbin/launchd
0 74 1 0 3:34PM ?? 0:00.89 /usr/sbin/syslogd
0 75 1 0 3:34PM ?? 0:01.42 /usr/libexec/UserEventAgent (System)
...
為 launched。這里將進(jìn)程 ID=1 的統(tǒng)稱為 init 進(jìn)程,它通常讀取與系統(tǒng)有關(guān)的初始化文件,并將系統(tǒng)引導(dǎo)到一個(gè)狀態(tài) (e.g. 多用戶),且不會(huì)終止,雖然運(yùn)行在用戶態(tài),但具有超級(jí)用戶權(quán)限。在孤兒進(jìn)程場(chǎng)景下,它負(fù)責(zé)做缺省的父進(jìn)程,關(guān)于這一點(diǎn)可以參考后面 "進(jìn)程終止" 一節(jié)。正因?yàn)檫M(jìn)程 ID 0 永遠(yuǎn)不可能分配給用戶進(jìn)程,所以它可以用作接口的臨界值,就如上面例子中 fork 所做的那樣,關(guān)于 fork 的詳細(xì)說(shuō)明可以參考后面 "進(jìn)程創(chuàng)建" 一節(jié)。
各種進(jìn)程 ID 通過(guò)下面的接口返回:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); // process ID
pid_t getppid(void); // parent process ID
uid_t getuid(void); // user ID
uid_t geteuid(void); // effect user ID
gid_t getgid(void); // group ID
gid_t getegid(void); // effect group ID
各個(gè)接口返回的 ID 已在注釋中說(shuō)明。進(jìn)程是動(dòng)態(tài)的程序文件、文件又由進(jìn)程生成,而它們都受系統(tǒng)中用戶和組的轄制,用戶態(tài)進(jìn)程必然屬于某個(gè)用戶和組,就像文件一樣,關(guān)于這一點(diǎn),可以參考這篇《[apue] linux 文件訪問(wèn)權(quán)限那些事兒 》。再說(shuō)深入一點(diǎn),用戶 ID、組 ID 標(biāo)識(shí)的是執(zhí)行進(jìn)程的用戶;有效用戶 ID、有效組 ID 則標(biāo)識(shí)了進(jìn)程程序文件通過(guò) set-user-id、set-group-id 標(biāo)識(shí)指定的用戶,一般用作權(quán)限"后門";還有 saved-set-uid、saved-set-gid,則用來(lái)恢復(fù)更改 uid、gid 之前的身份。關(guān)于兩組三種 ID 之間的關(guān)系、相互如何轉(zhuǎn)換及這樣做的目的,可以參考后面 "更改用戶 ID 和組 ID" 一節(jié)。
進(jìn)程創(chuàng)建
Unix 系統(tǒng)的進(jìn)程主要依賴 fork 創(chuàng)建:
#include <unistd.h>
pid_t fork(void);
fork 本意為分叉,像一條路突然分開(kāi)變成兩條一樣,調(diào)用 fork 后會(huì)裂變出兩個(gè)進(jìn)程,新進(jìn)程具有和原進(jìn)程完全相同的環(huán)境,包括執(zhí)行堆棧。即在調(diào)用 fork 處會(huì)產(chǎn)生兩次返回,一次是在父進(jìn)程,一次是在子進(jìn)程。
但是父、子進(jìn)程的返回值卻大不相同,父進(jìn)程返回的是成功創(chuàng)建的子進(jìn)程 ID;子進(jìn)程返回的是 0。通過(guò)上一節(jié)對(duì)進(jìn)程 ID 的說(shuō)明,0 是一個(gè)合法但不會(huì)分配給用戶進(jìn)程的 ID,這里作為區(qū)分父子進(jìn)程的關(guān)鍵,從而執(zhí)行不同的邏輯。父進(jìn)程 fork 返回子進(jìn)程的 pid 也是必要的,因?yàn)槟壳皼](méi)有一種接口可以返回父進(jìn)程所有的子進(jìn)程 ID,通過(guò) fork 返回值父進(jìn)程就可以得到子進(jìn)程的 ID;而反過(guò)來(lái),子進(jìn)程可以通過(guò) get_ppid 輕松獲取父進(jìn)程 ID,所以不需要在 fork 處返回,且為了區(qū)別于父進(jìn)程中的 fork 返回,這里有必要返回 0 來(lái)快速標(biāo)識(shí)自己是子進(jìn)程 (通過(guò)記錄父進(jìn)程 ID 等辦法也可以標(biāo)識(shí),但是明顯不如這種來(lái)得簡(jiǎn)潔)。
int pid = fork();
if (pid < 0)
{
// error
exit(1);
}
else if (pid == 0)
{
// child
printf ("%d spawn from %d\n", getpid(), getppid());
}
else
{
// parent
sleep (1);
printf ("%d create %d\n", getpid(), pid);
}
新建的子進(jìn)程具有和父進(jìn)程完全相同的數(shù)據(jù)空間、堆、棧,但這不意味著與父進(jìn)程共享,除代碼段這種只讀的區(qū)域,其他的都可以理解為父進(jìn)程的副本:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int g_count = 1;
int main()
{
int v_count = 42;
static int s_count = 1024;
int* h_count = (int*)malloc (sizeof (int));
*h_count = 36;
int pid = fork();
if (pid < 0)
{
// error
exit(1);
}
else if (pid == 0)
{
// child
g_count ++;
v_count ++;
s_count ++;
(*h_count) ++;
printf ("%d spawn from %d\n", getpid(), getppid());
}
else
{
// parent
sleep (1);
printf ("%d create %d\n", getpid(), pid);
}
printf ("%d: global %d, local %d, static %d, heap %d\n", getpid(), g_count, v_count, s_count, *h_count);
return 0;
}
這個(gè)例子就很說(shuō)明問(wèn)題,運(yùn)行得到下面的輸出:
$ ./forkit
18270 spawn from 18269
18270: global 2, local 43, static 1025, heap 37
18269 create 18270
18269: global 1, local 42, static 1024, heap 36
子進(jìn)程修改全局、局部、靜態(tài)、堆變量對(duì)父進(jìn)程不可見(jiàn),父、子進(jìn)程是相互隔離的,子進(jìn)程一般會(huì)在 fork 之后調(diào)用函數(shù)族來(lái)將進(jìn)程空間替換為新的程序文件。這就是 exec 函數(shù)族,它們會(huì)把當(dāng)前進(jìn)程內(nèi)容替換為磁盤上的程序文件并執(zhí)行新程序的代碼段,和 fork 是一對(duì)好搭檔。關(guān)于 exec 函數(shù)族的更多內(nèi)容,請(qǐng)參考后面 "exec" 一節(jié)。
對(duì)于習(xí)慣在 Windows 上創(chuàng)建進(jìn)程的用戶來(lái)說(shuō),CreateProcess 還是更容易理解一些,它直接把 fork + exec 的工作都包攬了,完全不知道還有復(fù)制進(jìn)程這種騷操作。那 Unix 為什么要繞這樣一個(gè)大彎呢?這是由于如果想在執(zhí)行新程序文件之前,對(duì)進(jìn)程屬性做一些設(shè)置,則必需在 fork 之后、exec 之前進(jìn)行處理,例如 I/O 重定向、設(shè)置用戶 ID 和組 ID、信號(hào)安排等等,而封裝成一整個(gè)的 CretaeProcess 對(duì)此是無(wú)能為力的,只能將這些代碼安排在新程序的開(kāi)頭才行,而有時(shí)新進(jìn)程的代碼是不受我們控制的,對(duì)此就無(wú)能為力了。
Unix 有沒(méi)有類似 CreateProcess 這樣的東西呢,也有,而且是在 POSIX 標(biāo)準(zhǔn)層面定義的:
#include <spawn.h>
int posix_spawn(pid_t *restrict pid, const char *restrict path,
const posix_spawn_file_actions_t *file_actions,
const posix_spawnattr_t *restrict attrp,
char *const argv[restrict], char *const envp[restrict]);
int posix_spawnp(pid_t *restrict pid, const char *restrict file,
const posix_spawn_file_actions_t *file_actions,
const posix_spawnattr_t *restrict attrp,
char *const argv[restrict], char * const envp[restrict]);
這就是 posix_spawn 和 posix_spawnp,兩者的參數(shù)完全相同,區(qū)別僅在于路徑參數(shù)是絕對(duì)路徑 (path) 還是帶搜索能力的相對(duì)路徑 (file)。不過(guò)這個(gè)接口無(wú)意取代 fork + exec,僅用來(lái)支持對(duì)存儲(chǔ)管理缺少硬件支持的系統(tǒng),這種系統(tǒng)通常難以有效的實(shí)現(xiàn) fork。
有的人認(rèn)為基于 fork+exec 的 posix_spawn 不如 CreateProcess 性能好,畢竟要復(fù)制父進(jìn)程一大堆東西,而大部分對(duì)新進(jìn)程又無(wú)用。實(shí)際上 Unix 采取了兩個(gè)策略,導(dǎo)致 fork+exec 也不是那么低效,通常情況下都能媲美 CreateProcess。這些策略分別是寫時(shí)復(fù)制 (COW:Copy-On-Write) 與 vfork。
COW
fork 之后并不執(zhí)行一個(gè)父進(jìn)程數(shù)據(jù)段、棧、堆的完全復(fù)制,作為替代,這些區(qū)域由父、子進(jìn)程共享,并且內(nèi)核將它們的訪問(wèn)權(quán)限標(biāo)記為只讀。如果父、子進(jìn)程中的任一個(gè)試圖修改這些區(qū)域,則內(nèi)核只為修改區(qū)域的那塊內(nèi)存制作一個(gè)副本,通常是虛擬存儲(chǔ)器系統(tǒng)中的一頁(yè)。在更深入的說(shuō)明這個(gè)技術(shù)之前,先來(lái)看看 Linux 是如何將虛擬地址轉(zhuǎn)換為物理地址的:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdint.h>
unsigned long virtual2physical(void* ptr)
{
unsigned long vaddr = (unsigned long)ptr;
int pageSize = getpagesize();
unsigned long v_pageIndex = vaddr / pageSize;
unsigned long v_offset = v_pageIndex * sizeof(uint64_t);
unsigned long page_offset = vaddr % pageSize;
uint64_t item = 0;
int fd = open("/proc/self/pagemap", O_RDONLY);
if(fd == -1)
{
printf("open /proc/self/pagemap error\n");
return NULL;
}
if(lseek(fd, v_offset, SEEK_SET) == -1)
{
printf("sleek error\n");
return NULL;
}
if(read(fd, &item, sizeof(uint64_t)) != sizeof(uint64_t))
{
printf("read item error\n");
return NULL;
}
if((((uint64_t)1 << 63) & item) == 0)
{
printf("page present is 0\n");
return NULL;
}
uint64_t phy_pageIndex = (((uint64_t)1 << 55) - 1) & item;
return (unsigned long)((phy_pageIndex * pageSize) + page_offset);
}
這段代碼可以在用戶空間將一個(gè)虛擬內(nèi)存地址轉(zhuǎn)換為一個(gè)物理地址,具體原理就不介紹了,感興趣的請(qǐng)參考附錄 2。用它做個(gè)小測(cè)試:
void test_ptr(void *ptr, char const* prompt)
{
uint64_t addr = virtual2physical(ptr);
printf ("%s: virtual: 0x%x, physical: 0x%x\n", prompt, ptr, addr);
}
int g_val1=0;
int g_val2=1;
int main(void) {
test_ptr(&g_val1, "global1");
test_ptr(&g_val2, "global2");
int l_val3=2;
int l_val4=3;
test_ptr(&l_val3, "local1");
test_ptr(&l_val4, "local2");
static int s_val5=4;
static int s_val6=5;
test_ptr(&s_val5, "static1");
test_ptr(&s_val6, "static2");
int *h_val7=(int*)malloc(sizeof(int));
int *h_val8=(int*)malloc(sizeof(int));
test_ptr(h_val7, "heap1");
test_ptr(h_val8, "heap2");
free(h_val7);
free(h_val8);
return 0;
};
測(cè)試種類還是比較豐富的,有局部變量、靜態(tài)變量、全局變量和堆上分配的變量。在 CentOS 上有以下輸出:
> sudo ./memtrans
global1: virtual: 0x60107c, physical: 0x8652f07c
global2: virtual: 0x60106c, physical: 0x8652f06c
local1: virtual: 0x9950ff2c, physical: 0xfb1df2c
local2: virtual: 0x9950ff28, physical: 0xfb1df28
static1: virtual: 0x601070, physical: 0x8652f070
static2: virtual: 0x601074, physical: 0x8652f074
heap1: virtual: 0xc3e010, physical: 0xb7ebe010
heap2: virtual: 0xc3e030, physical: 0xb7ebe030
發(fā)現(xiàn)以下幾點(diǎn):
- 同類型的變量虛擬、物理地址相差不大
- 靜態(tài)和全局變量虛擬地址相近、物理地址也相近,很可能是分配在同一個(gè)頁(yè)上了
- 局部、全局、堆上的變量虛擬地址相差較大、物理地址也相差較大,應(yīng)該是分配在不同的頁(yè)上了
必需使用超級(jí)用戶權(quán)限執(zhí)行這段程序,否則看起來(lái)不是那么正確:
> ./memtrans
global1: virtual: 0x60107c, physical: 0x7c
global2: virtual: 0x60106c, physical: 0x6c
local1: virtual: 0x6a433e2c, physical: 0xe2c
local2: virtual: 0x6a433e28, physical: 0xe28
static1: virtual: 0x601070, physical: 0x70
static2: virtual: 0x601074, physical: 0x74
heap1: virtual: 0x116b010, physical: 0x10
heap2: virtual: 0x116b030, physical: 0x30
雖然 virtual2physical 沒(méi)有報(bào)錯(cuò),但是一眼看上去這個(gè)結(jié)果就是有問(wèn)題的。能將虛擬地址轉(zhuǎn)化為物理地址后,就可以拿它在 fork 的場(chǎng)景中做個(gè)驗(yàn)證:
int g_count = 1;
int main()
{
int v_count = 42;
static int s_count = 1024;
int* h_count = (int*)malloc (sizeof (int));
*h_count = 36;
printf ("%d: global ptr 0x%x:0x%x, local ptr 0x%x:0x%x, static ptr 0x%x:0x%x, heap ptr 0x%x:0x%x\n", getpid(),
&g_count, virtual2physical(&g_count),
&v_count, virtual2physical(&v_count),
&s_count, virtual2physical(&s_count),
h_count, virtual2physical(h_count));
int pid = fork();
if (pid < 0)
{
// error
exit(1);
}
else if (pid == 0)
{
// child
printf ("%d spawn from %d\n", getpid(), getppid());
#if 0
g_count ++;
v_count ++;
s_count ++;
(*h_count) ++;
#endif
}
else
{
// parent
sleep (1);
printf ("%d create %d\n", getpid(), pid);
}
printf ("%d: global %d, local %d, static %d, heap %d\n", getpid(), g_count, v_count, s_count, *h_count);
printf ("%d: global ptr 0x%x:0x%x, local ptr 0x%x:0x%x, static ptr 0x%x:0x%x, heap ptr 0x%x:0x%x\n", getpid(),
&g_count, virtual2physical(&g_count),
&v_count, virtual2physical(&v_count),
&s_count, virtual2physical(&s_count),
h_count, virtual2physical(h_count));
return 0;
}
增加了對(duì)虛擬、物理地址的打印,并屏蔽了子進(jìn)程對(duì)變量的修改,先看看父、子進(jìn)程是否共享了內(nèi)存頁(yè):
> sudo ./forkit
19216: global ptr 0x60208c:0x5769308c, local ptr 0x22c50040:0xf4fe2040, static ptr 0x602090:0x57693090, heap ptr 0x1e71010:0x89924010
19217 spawn from 19216
19217: global 1, local 42, static 1024, heap 36
19217: global ptr 0x60208c:0x5769308c, local ptr 0x22c50040:0xf4fe2040, static ptr 0x602090:0x57693090, heap ptr 0x1e71010:0x89924010
19216 create 19217
19216: global 1, local 42, static 1024, heap 36
19216: global ptr 0x60208c:0x412f308c, local ptr 0x22c50040:0xea994040, static ptr 0x602090:0x412f3090, heap ptr 0x1e71010:0x89924010
發(fā)現(xiàn)以下現(xiàn)象:
- 所有變量虛擬地址不變
- 僅堆變量的物理地址不變
- 子進(jìn)程所有變量的物理地址不變,父進(jìn)程局部、靜態(tài)、全局變量的物理地址發(fā)生了改變
從現(xiàn)象可以得到以下結(jié)論:
- COW 生效,否則堆變量的物理地址不可能不變
- 局部、靜態(tài)、全局變量的物理地址發(fā)生改變很可能是因?yàn)樵擁?yè)上有其它數(shù)據(jù)發(fā)生了變更需要復(fù)制
- 率先復(fù)制的那一方物理地址會(huì)發(fā)生變更
下面再看下子進(jìn)程修改變量的情況:
> sudo ./forkit
23182: global ptr 0x60208c:0x1037008c, local ptr 0x677e8540:0xe65b6540, static ptr 0x602090:0x10370090, heap ptr 0x252d010:0x9fb3d010
23183 spawn from 23182
23183: global 2, local 43, static 1025, heap 37
23183: global ptr 0x60208c:0x1037008c, local ptr 0x677e8540:0xe65b6540, static ptr 0x602090:0x10370090, heap ptr 0x252d010:0x6dafb010
23182 create 23183
23182: global 1, local 42, static 1024, heap 36
23182: global ptr 0x60208c:0xf045708c, local ptr 0x677e8540:0x5bc6f540, static ptr 0x602090:0xf0457090, heap ptr 0x252d010:0x9fb3d010
這下所有變量的物理地址都改變了,進(jìn)一步驗(yàn)證了 COW 的介入,特別是子進(jìn)程堆變量物理地址改變 (0x6dafb010) 而父進(jìn)程的沒(méi)有改變 (0x9fb3d010),說(shuō)明系統(tǒng)確實(shí)為修改頁(yè)的一方分配了新的頁(yè)。另一方面,子進(jìn)程修改了局部、靜態(tài)、全局變量而物理地址沒(méi)有發(fā)生改變,則說(shuō)明當(dāng)頁(yè)不再標(biāo)記為共享后,子進(jìn)程再修改這些頁(yè)也不會(huì)為它重新分配頁(yè)了。最后父進(jìn)程沒(méi)有修改局部、靜態(tài)、全局變量而物理地址發(fā)生了變化,一定是這些變量所在頁(yè)的其它部分被修改導(dǎo)致的,且這些修改發(fā)生在用戶修改這些變量之前,即 fork 內(nèi)部。
vfork
另外一種提高 fork 性能的方法是 vfork:
#include <unistd.h>
pid_t vfork(void);
它的聲明與 fork 完全一致,用法也差不多,但是卻有以下根本不同:
- 父、子進(jìn)程并不進(jìn)行任何數(shù)據(jù)段、棧、堆的復(fù)制,連 COW 都沒(méi)有,完全是共享同樣的內(nèi)存空間
- 父進(jìn)程只有在子進(jìn)程調(diào)用 exec 或 exit 之后才能繼續(xù)運(yùn)行
vfork 是面向 fork+exec 使用場(chǎng)景的優(yōu)化,所以在 exec (或 exit) 之前,子進(jìn)程就是在父進(jìn)程的地址空間運(yùn)行的。而為了避免父、子進(jìn)程訪問(wèn)同一個(gè)內(nèi)存頁(yè)導(dǎo)致的競(jìng)爭(zhēng)問(wèn)題,父進(jìn)程在此期間會(huì)被短暫掛起,預(yù)期子進(jìn)程會(huì)立刻調(diào)用 exec,所以這個(gè)延遲還是可以接受的。修改上面的 forkit 代碼:
#if 0
int pid = fork();
#else
int pid = vfork();
#endif
使用 vfork 代替 fork,再來(lái)觀察結(jié)果有何不同:
> sudo ./forkit
15421: global ptr 0x60208c:0x9f6d608c, local ptr 0x91d548c0:0xa98148c0, static ptr 0x602090:0x9f6d6090, heap ptr 0x1cc1010:0xf3a5c010
15422 spawn from 15421
15422: global 2, local 43, static 1025, heap 37
15422: global ptr 0x60208c:0x9f6d608c, local ptr 0x91d548c0:0xa98148c0, static ptr 0x602090:0x9f6d6090, heap ptr 0x1cc1010:0xf3a5c010
15421 create 15422
Segmentation fault
子進(jìn)程運(yùn)行正常而父進(jìn)程在 fork 返回后崩潰了,打開(kāi) gdb 掛上 coredmp 文件查看:
> sudo gdb ./forkit --core=core.15421
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /ext/code/apue/08.chapter/forkit...done.
[New LWP 15421]
Core was generated by `./forkit'.
Program terminated with signal 11, Segmentation fault.
#0 0x0000000000400ace in main () at forkit.c:90
90 printf ("%d: global %d, local %d, static %d, heap %d\n", getpid(), g_count, v_count, s_count, *h_count);
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64
(gdb) i lo
v_count = 43
s_count = 1025
h_count = 0x0
pid = 15422
(gdb)
因?yàn)樯傻?core 文件具有 root 權(quán)限,所以這里也使用 sudo 提權(quán)。打印本地變量查看,發(fā)現(xiàn) h_count 指針為空了,導(dǎo)致 printf 崩潰。再看 vfork 的使用說(shuō)明,發(fā)現(xiàn)有下面這么一段:
vfork() differs from fork(2) in that the calling thread is suspended until the child terminates (either normally, by calling
_exit(2), or abnormally, after delivery of a fatal signal), or it makes a call to execve(2). Until that point, the child shares all
memory with its parent, including the stack. The child must not return from the current function or call exit(3), but may call
_exit(2).
大意是說(shuō)因 vfork 后子進(jìn)程甚至?xí)蚕砀高M(jìn)程執(zhí)行堆棧,所以子進(jìn)程不能通過(guò) return 和 exit 退出,只能通過(guò) _exit。嘖嘖,一不小心就踩了坑,修改代碼如下:
if (pid < 0)
{
// error
exit(1);
}
else if (pid == 0)
{
// child
printf ("%d spawn from %d\n", getpid(), getppid());
#if 1
g_count ++;
v_count ++;
s_count ++;
(*h_count) ++;
#endif
printf ("%d: global %d, local %d, static %d, heap %d\n", getpid(), g_count, v_count, s_count, *h_count);
printf ("%d: global ptr 0x%x:0x%x, local ptr 0x%x:0x%x, static ptr 0x%x:0x%x, heap ptr 0x%x:0x%x\n", getpid(),
&g_count, virtual2physical(&g_count),
&v_count, virtual2physical(&v_count),
&s_count, virtual2physical(&s_count),
h_count, virtual2physical(h_count));
_exit(0);
}
else
{
// parent
// sleep (1);
printf ("%d create %d\n", getpid(), pid);
}
主要修改點(diǎn)如下:
- 打印語(yǔ)句復(fù)制一份到子進(jìn)程
- 子進(jìn)程通過(guò) _exit 退出
- 父進(jìn)程去除 sleep 調(diào)用
再次編譯運(yùn)行:
> sudo ./forkit
22831: global ptr 0x60208c:0xde9ee08c, local ptr 0x9c8a3ac0:0x2661dac0, static ptr 0x602090:0xde9ee090, heap ptr 0x1a90010:0x88797010
22832 spawn from 22831
22832: global 2, local 43, static 1025, heap 37
22832: global ptr 0x60208c:0xde9ee08c, local ptr 0x9c8a3ac0:0x2661dac0, static ptr 0x602090:0xde9ee090, heap ptr 0x1a90010:0x88797010
22831 create 22832
22831: global 2, local 43, static 1025, heap 37
22831: global ptr 0x60208c:0xde9ee08c, local ptr 0x9c8a3ac0:0x2661dac0, static ptr 0x602090:0xde9ee090, heap ptr 0x1a90010:0x88797010
這回不崩潰了,而且可以看到以下有趣的現(xiàn)象:
- 父進(jìn)程的所有變量都被子進(jìn)程修改了
- 父、子進(jìn)程的所有變量虛擬、物理地址完全一致
進(jìn)一步印證了上面的結(jié)論。由于 vfork 根本不存在內(nèi)存空間的復(fù)制,所以理論上它是性能最高的,畢竟 COW 在底層還是發(fā)生了很多內(nèi)存頁(yè)復(fù)制的。
vfork 這個(gè)接口是屬于 SUS 標(biāo)準(zhǔn)的,目前流行的 Unix 都支持,只不過(guò)它被標(biāo)識(shí)為了廢棄,使用時(shí)需要小心,尤其是處理子進(jìn)程的退出。
fork + fd
子進(jìn)程會(huì)繼承父進(jìn)程以下屬性:
- 打開(kāi)文件描述符
- 實(shí)際用戶 ID、實(shí)際組 ID、有效用戶 ID、有效組 ID
- 附加組 ID
- 進(jìn)程組 ID
- 會(huì)話 ID
- 控制終端
- 設(shè)置用戶 ID 標(biāo)志和設(shè)置組 ID 標(biāo)志
- 當(dāng)前工作目錄
- 根目錄
- 文件模式創(chuàng)建屏蔽字
- 信號(hào)屏蔽和安排
- 打開(kāi)文件描述符的 close-on-exec 標(biāo)志
- 環(huán)境變量
- 連接的共享存儲(chǔ)段
- 存儲(chǔ)映射
- 資源限制
- ……
以打開(kāi)文件描述符為例,有如下測(cè)試程序:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
printf ("before fork\n");
int pid = fork();
if (pid < 0)
{
// error
exit(1);
}
else if (pid == 0)
{
// child
printf ("%d spawn from %d\n", getpid(), getppid());
}
else
{
// parent
sleep (1);
printf ("%d create %d\n", getpid(), pid);
}
printf ("after fork\n");
return 0;
}
運(yùn)行程序輸出如下:
> ./forkfd
before fork
7204 spawn from 7203
after fork
7203 create 7204
after fork
before fork 只在父進(jìn)程輸出一次,符合預(yù)期,如果在 main 函數(shù)第一行插入以下代碼:
setvbuf (stdout, NULL, _IOFBF, 0);
將標(biāo)準(zhǔn)輸出設(shè)置為全緩沖模式,(關(guān)于標(biāo)準(zhǔn) IO 的緩沖模式,可以參考這篇《[apue] 標(biāo)準(zhǔn) I/O 庫(kù)那些事兒 》),則輸出會(huì)發(fā)生改變:
> ./forkfd
before fork
6955 spawn from 6954
after fork
before fork
6954 create 6955
after fork
可以看到 before fork 這條語(yǔ)句輸出了兩次,分別在父、子進(jìn)程各輸出一次,這是由于 stdout 由行緩沖變更為全緩沖后,積累的內(nèi)容并不隨換行符 flush,從而就會(huì)被 fork 復(fù)制到子進(jìn)程,并與子進(jìn)程生成的信息一起 flush 到控制臺(tái),最終輸出兩次。如果仍保持行緩沖模式,還會(huì)導(dǎo)致多次輸出嗎?答案是有可能,只要將上面的換行符去掉就可以:
printf ("before fork ");
新的輸出如下:
> ./forkfd
before fork 17736 spawn from 17735
after fork
before fork 17735 create 17736
after fork
原理是一樣的。其實(shí)還存在另外的隱式修改標(biāo)準(zhǔn)輸出緩沖方式的辦法:文件重定向,仍以有換行符的版本為例:
> ./forkfd > output.txt
> cat output.txt
before fork
15505 spawn from 15504
after fork
before fork
15504 create 15505
after fork
通過(guò)將標(biāo)準(zhǔn)輸出重定向到 output.txt 文件,實(shí)現(xiàn)了行緩沖到全緩沖的變化,從而得到了與調(diào)用 setvbuf 相同的結(jié)果。使用不帶緩沖的 write、或者在 fork 前主動(dòng) flush 緩沖,以避免上面的問(wèn)題。
除了緩存復(fù)制,父、子進(jìn)程共享打開(kāi)文件描述符的另外一個(gè)問(wèn)題是讀寫競(jìng)爭(zhēng),fork 后父、子進(jìn)程共享文件句柄的情況如下圖 (參考《[apue] 一圖讀懂 unix 文件句柄及文件共享過(guò)程 》):
父、子進(jìn)程共享文件句柄特別像進(jìn)程內(nèi) dup 的情況,此時(shí)對(duì)于共享的雙方而言,任一進(jìn)程更新文件偏移量對(duì)另一個(gè)進(jìn)程都是可見(jiàn)的,保證了一個(gè)進(jìn)程添加的數(shù)據(jù)會(huì)在另一個(gè)進(jìn)程之后。但如果不做任何同步,它們的數(shù)據(jù)會(huì)相互混合,從而使輸出變得混亂。一般遵循以下慣例來(lái)保證父、子進(jìn)程不會(huì)在共享的文件句柄上產(chǎn)生讀寫競(jìng)爭(zhēng):
- 父進(jìn)程等待子進(jìn)程完成
- 父、子進(jìn)程各自執(zhí)行不同的程序段 (關(guān)閉各自不需要使用的文件描述符)
如果必需使用共享的文件句柄,則需要引入進(jìn)程間同步機(jī)制來(lái)解決讀寫沖突,關(guān)于這一點(diǎn),可以參考后續(xù) "父子進(jìn)程同步" 的文章。
在上一節(jié)介紹 vfork 時(shí),了解到它是不復(fù)制進(jìn)程空間的,子進(jìn)程需要保證在退出時(shí)使用 _exit 來(lái)清理進(jìn)程,避免 return 語(yǔ)句破壞棧指針。這里有個(gè)疑問(wèn),如果使用 exit 代替上例中的 _exit 會(huì)如何呢?修改上面的程序進(jìn)行驗(yàn)證:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
setvbuf (stdout, NULL, _IOFBF, 0);
printf ("before fork\n");
int pid = vfork();
if (pid < 0)
{
// error
exit(1);
}
else if (pid == 0)
{
// child
printf ("%d spawn from %d\n", getpid(), getppid());
exit(0);
}
else
{
// parent
printf ("%d create %d\n", getpid(), pid);
}
printf ("after fork\n");
return 0;
}
發(fā)現(xiàn)父進(jìn)程可以正常終止:
> ./forkfd
before fork
25923 spawn from 25922
25922 create 25923
after fork
_exit 是不會(huì)做任何清理工作的,所以是安全的;exit 至少會(huì) flush 標(biāo)準(zhǔn) IO,至于是否關(guān)閉它們則沒(méi)有標(biāo)準(zhǔn)明確的要求這一點(diǎn),由各個(gè)實(shí)現(xiàn)自行決定。如果 exit 關(guān)閉了標(biāo)準(zhǔn) IO,那么父進(jìn)程一定無(wú)法輸出 after fork 這句,可見(jiàn) CentOS 上的exit 沒(méi)有關(guān)閉標(biāo)準(zhǔn) IO。目前大多數(shù)系統(tǒng)的 exit 實(shí)現(xiàn)不在這方面給自己找麻煩,畢竟進(jìn)程結(jié)束時(shí)系統(tǒng)會(huì)自動(dòng)關(guān)閉進(jìn)程打開(kāi)的所有文件句柄,在庫(kù)中關(guān)閉它們,只是增加了開(kāi)銷而不會(huì)帶來(lái)任何益處。
apue 原文講,即使 exit 關(guān)閉了標(biāo)準(zhǔn) IO,STDOUT_FILENO 句柄還是可用的,通過(guò) write 仍可以正常輸出,子進(jìn)程關(guān)閉自己的標(biāo)準(zhǔn) IO 句柄并不影響父進(jìn)程的那一份,對(duì)此進(jìn)行驗(yàn)證:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
printf ("before fork\n");
char buf[128] = { 0 };
int pid = vfork();
if (pid < 0)
{
// error
exit(1);
}
else if (pid == 0)
{
// child
printf ("%d spawn from %d\n", getpid(), getppid());
fclose (stdin);
fclose (stdout);
fclose (stderr);
exit(0);
}
else
{
// parent
sprintf (buf, "%d create %d\n", getpid(), pid);
write (STDOUT_FILENO, buf, strlen(buf));
}
sprintf (buf, "after fork\n");
write (STDOUT_FILENO, buf, strlen(buf));
return 0;
}
主要修改點(diǎn)有三處:
- 去除標(biāo)準(zhǔn)輸出重定向
- 在 child exit 前主動(dòng)關(guān)閉標(biāo)準(zhǔn) IO 庫(kù)
- 在 parent vfork 返回后,使用 write 代替 printf 打印日志
新的輸出如下:
> ./forkfd
before fork
20910 spawn from 20909
20909 create 20910
after fork
和書上說(shuō)的一致,看來(lái)關(guān)閉標(biāo)準(zhǔn) IO 庫(kù)只影響父進(jìn)程的 printf 調(diào)用,不影響 write 調(diào)用。再試試直接關(guān)閉文件句柄:
close (STDIN_FILENO);
close (STDOUT_FILENO);
close (STDERR_FILENO);
新的輸出如下:
> ./forkfd
before fork
17462 spawn from 17461
17461 create 17462
after fork
仍然沒(méi)有影響!看起來(lái) vfork 子進(jìn)程雖然沒(méi)有復(fù)制任何父進(jìn)程空間的內(nèi)容,但句柄仍是做了 dup 的,所以關(guān)閉子進(jìn)程的任何句柄,對(duì)父進(jìn)程沒(méi)有影響。
標(biāo)準(zhǔn) IO (stdin/stdout/stderr) 還和文件句柄不同,它們帶有一些額外信息例如緩存等是存儲(chǔ)在堆或棧上的,如果 vfork 后子進(jìn)程的 exit 關(guān)閉了它們,父進(jìn)程是會(huì)受到影響的,這進(jìn)一步反證了 exit 不會(huì)關(guān)閉標(biāo)準(zhǔn) IO。
關(guān)于子進(jìn)程繼承父進(jìn)程的其它屬性,這里就不一一驗(yàn)證了,有興趣的讀者可以自行構(gòu)造 demo。最后補(bǔ)充一下 fork 后子進(jìn)程與父進(jìn)程不同的屬性:
- fork 返回值
- 進(jìn)程 ID
- 父進(jìn)程 ID
- 子進(jìn)程的 CPU 時(shí)間 (tms_utime / tms_stime / tms_cutime / tms_ustime 均置為 0)
- 文件鎖不會(huì)繼承
- 未處理的鬧鐘 (alarm) 將被清除
- 未處理的信號(hào)集將設(shè)置為空
- ……
clone
在 fork 的全復(fù)制和 vfork 全不復(fù)制之間,有沒(méi)有一個(gè)接口可以自由定制進(jìn)程哪些信息需要復(fù)制?答案是 clone,不過(guò)這個(gè)是 Linux 特有的:
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,int flags, void *arg, .../* pid_t *ptid, void *newtls, pid_t *ctid */ );
與 fork 不同,clone 子進(jìn)程啟動(dòng)時(shí)將運(yùn)行用戶提供的 fn(arg) ,并且需要用戶提前開(kāi)辟好??臻g (child_stack),而控制各種信息共享就是通過(guò) flags 參數(shù)了,下面列一些主要的控制參數(shù):
- CLONE_FILES:是否共享文件句柄
- CLONE_FS:是否共享文件系統(tǒng)相關(guān)信息,這些信息由 chroot、chdir、umask 指定
- CLONE_NEWIPC:是否共享 IPC 命名空間
- CLONE_PID:是否共享 PID
- CLONE_SIGHAND:是否共享信號(hào)處理
- CLONE_THREAD:是否共享相同的線程組
- CLONE_VFORK:是否在子進(jìn)程 exit 或 execve 之前掛起父進(jìn)程
- CLONE_VM:是否共享同一地址空間
- ……
其實(shí) glibc clone 底層依賴的 clone 系統(tǒng)調(diào)用 (sys_clone) 接口更接近于 fork 系統(tǒng)調(diào)用,glibc 僅僅是在 sys_clone 的子進(jìn)程返回中調(diào)用用戶提供的 fn(arg) 而已。它將 fork 中的各種進(jìn)程信息是否共享的決定權(quán)交給了用戶,所以有更大的靈活性,甚至可以基于 clone 實(shí)現(xiàn)用戶態(tài)線程庫(kù)。上一節(jié)中說(shuō) vfork 后子進(jìn)程在退出時(shí)可以關(guān)閉 STDOUT_FILENO 而不影響父進(jìn)程,這是因?yàn)闃?biāo)準(zhǔn) IO 句柄是經(jīng)過(guò) vfork dup 的,如果使用 clone 并指定共享父進(jìn)程的文件句柄 (CLONE_FILES) 會(huì)如何?下面寫個(gè)例子進(jìn)行驗(yàn)證:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
int child_func(void *arg)
{
// child
printf ("%d spawned from %d\n", getpid(), getppid());
return 1;
}
int main()
{
printf ("before fork\n");
size_t stack_size = 1024 * 1024;
char *stack = (char *)malloc (stack_size);
int pid = clone(child_func, stack+stack_size, CLONE_VM | CLONE_VFORK | SIGCHLD, 0);
if (pid < 0)
{
// error
exit(1);
}
// parent
printf ("[1] %d create %d\n", getpid(), pid);
char buf[128] = { 0 };
sprintf (buf, "[2] %d create %d\n", getpid(), pid);
write (STDOUT_FILENO, buf, strlen(buf));
return 0;
}
先演示下不加 CLONE_FILES 的效果:
> ./clonefd
before fork
1271 spawned from 1270
[1] 1270 create 1271
[2] 1270 create 1271
這個(gè)和 vfork 效果相同。這里為了驗(yàn)證標(biāo)準(zhǔn) IO 庫(kù)被關(guān)閉的情況,父進(jìn)程最后一句日志使用兩種方法打印,能輸出兩行就證明標(biāo)準(zhǔn) IO 和底層句柄都沒(méi)有被關(guān)閉,不同的方法使用前綴數(shù)字進(jìn)行區(qū)別。
clone 在這個(gè)場(chǎng)景的使用有幾點(diǎn)需要注意:
- 至少需要為 clone 指定 CLONE_VM 選項(xiàng),用于父、子進(jìn)程共享內(nèi)存地址空間
- 指定的 stack 地址是開(kāi)辟內(nèi)存地址的末尾,因?yàn)闂J窍蛏显鲩L(zhǎng)的,剛開(kāi)始 child 進(jìn)程一啟動(dòng)就掛掉,就是這里沒(méi)設(shè)置對(duì)
- 指定 CLONE_VFORK 標(biāo)記,這樣父進(jìn)程會(huì)在子進(jìn)程退出后才繼續(xù)運(yùn)行,避免了多余的 sleep
在子進(jìn)程關(guān)閉標(biāo)準(zhǔn) IO 庫(kù)嘗試:
> ./clonefd
before fork
5433 spawned from 5432
[2] 5432 create 5433
父進(jìn)程的 printf 不工作但 write 可以工作,符合預(yù)期。在子進(jìn)程關(guān)閉 STDOUT_FILENO 嘗試:
> ./clonefd
before fork
11688 spawned from 11687
[1] 11687 create 11688
[2] 11687 create 11688
兩個(gè)都能打印,證實(shí)了 fd 是經(jīng)過(guò) dup 的,與之前 vfork 的結(jié)果完全一致。下面為 clone 增加一個(gè)共享文件描述表的設(shè)置:
int pid = clone(child_func, stack+stack_size, CLONE_VM | CLONE_VFORK | CLONE_FILES | SIGCHLD, 0);
再運(yùn)行上面兩個(gè)用例:
> ./clonefd
before fork
8676 spawned from 8675
兩個(gè)場(chǎng)景父進(jìn)程的 printf 與 write 都不輸出了,但是原理稍有差別,前者是因?yàn)殛P(guān)閉標(biāo)準(zhǔn) IO 對(duì)象后底層的句柄也被關(guān)閉了;后者是雖然標(biāo)準(zhǔn) IO 對(duì)象雖然還打開(kāi)著,但底層的句柄已經(jīng)失效了,所以也無(wú)法輸出信息。
clone 雖然強(qiáng)大但不具備可移植性,唯一與它類似的是 FreeBSD 上的 rfork。
fork + pthread
fork 并不復(fù)制進(jìn)程的線程信息,請(qǐng)看下例:
#include "../apue.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <pthread.h>
#include <errno.h>
static void* thread_start (void *arg)
{
printf ("thread start %lu\n", pthread_self ());
sleep (2);
printf ("thread exit %lu\n", pthread_self ());
return 0;
}
int main (int argc, char *argv[])
{
int ret = 0;
pthread_t tid = 0;
ret = pthread_create (&tid, NULL, &thread_start, NULL);
if (ret != 0)
err_sys ("pthread_create");
pid_t pid = 0;
if ((pid = fork ()) < 0)
err_sys ("fork error");
else if (pid == 0)
{
printf ("[%u] child running, thread %lu\n", getpid(), pthread_self());
sleep (3);
}
else
{
printf ("fork and exec child %u in thread %lu\n", pid, pthread_self());
sleep (4);
}
exit (0);
}
做個(gè)簡(jiǎn)單說(shuō)明:
- 父進(jìn)程啟動(dòng)一個(gè)線程 (thread_start)
- 線程啟動(dòng)后休眠 2 秒
- 父進(jìn)程啟動(dòng)一個(gè)子進(jìn)程,子進(jìn)程啟動(dòng)后休眠 3 秒后退出
- 父進(jìn)程休眠 4 秒后退出
執(zhí)行程序有如下輸出:
> ./fork_pthread
fork and exec child 9825 in thread 140542546036544
thread start 140542537676544
[9825] child running, thread 140542546036544
thread exit 140542537676544
> ./fork_pthread
fork and exec child 28362 in thread 139956664842048
[28362] child running, thread 139956664842048
thread start 139956656482048
thread exit 139956656482048
注意這個(gè) threadid,長(zhǎng)長(zhǎng)的一串首尾相同,容易讓人誤認(rèn)為是同一個(gè) thread,實(shí)際上兩個(gè)是不同的,體現(xiàn)在中間的差異,以第二次執(zhí)行的輸出為例,一個(gè)是 6484,另一個(gè)是 5648,猛的一眼看上去不容易看出來(lái),坑爹~
兩次運(yùn)行線程的啟動(dòng)和子進(jìn)程的啟動(dòng)順序有別,但結(jié)果都是一樣的,子進(jìn)程沒(méi)有觀察到線程的退出日志,從而可以斷定沒(méi)有復(fù)制父進(jìn)程的線程信息。對(duì)上面的例子稍加改造,看看在線程中 fork 子進(jìn)程會(huì)如何:
#include "../apue.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <pthread.h>
#include <errno.h>
static void* thread_start (void *arg)
{
printf ("thread start %lu\n", pthread_self ());
pid_t pid = 0;
if ((pid = fork ()) < 0)
err_sys ("fork error");
else if (pid == 0)
{
printf ("[%u] child running, thread %lu\n", getpid(), pthread_self());
sleep (3);
}
else
{
printf ("fork and exec child %u in thread %lu\n", pid, pthread_self());
sleep (2);
}
printf ("thread exit %lu\n", pthread_self ());
return 0;
}
int main (int argc, char *argv[])
{
int ret = 0;
pthread_t tid = 0;
ret = pthread_create (&tid, NULL, &thread_start, NULL);
if (ret != 0)
err_sys ("pthread_create");
sleep (4);
printf ("main thread exit %lu\n", pthread_self());
exit (0);
}
重新執(zhí)行:
> ./fork_pthread
thread start 139848844396288
fork and exec child 17141 in thread 139848844396288
[17141] child running, thread 139848844396288
thread exit 139848844396288
thread exit 139848844396288
main thread exit 139848852756288
發(fā)現(xiàn)這次只復(fù)制了新線程 (4439),沒(méi)有復(fù)制主線程 (5275),仍然是不完整的。不過(guò) POSIX 語(yǔ)義本來(lái)如此:只復(fù)制 fork 所在的線程,如果想復(fù)制進(jìn)程的所有線程信息,目前僅有 Solaris 系統(tǒng)能做到,而且只對(duì) Solaris 線程有效,POSIX 線程仍保持只復(fù)制一個(gè)的語(yǔ)義。而為了和 POSIX 語(yǔ)義一致 (即只復(fù)制一個(gè) Solaris 線程),它特意推出了 fork1 接口干這件事,看來(lái)復(fù)制全部線程反而是個(gè)小眾需求。
exec
exec 函數(shù)族并不創(chuàng)建新的進(jìn)程,只是用一個(gè)全新的程序替換了當(dāng)前進(jìn)程的正文、數(shù)據(jù)、堆和棧段,所以調(diào)用前后進(jìn)程 ID 并不改變。函數(shù)族共包含六個(gè)原型:
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *file, char *const argv[], char *const envp[]);
不同的后綴有不同的含義:
- l:使用可變參數(shù)列表傳遞新程序參數(shù) (list),一般需要配合 va_arg / va_start / va_end 來(lái)提取參數(shù)
- v:與 l 參數(shù)相反,使用參數(shù)數(shù)組傳遞新程序參數(shù) (vector)
- p:傳遞程序文件名而非路徑,如果 file 參數(shù)不包含 / 字符,則在 PATH 環(huán)境變量中搜索可執(zhí)行文件
- e:指定環(huán)境變量數(shù)組 envp 參數(shù)而不是默認(rèn)的 environ 變量作為新程序的環(huán)境變量
書上有個(gè)圖很好的解釋了它們的之間的關(guān)系:
做個(gè)簡(jiǎn)單說(shuō)明:
- 所有 l 后綴的接口,將參數(shù)列表提取為數(shù)組后調(diào)用 v 后綴的接口
- execvp 在 PATH 環(huán)境變量中查找可執(zhí)行文件,確認(rèn)新程序路徑后調(diào)用 execv
- execv 使用 environ 全局變量作為 envp 參數(shù)調(diào)用 execve
百川入海,execve 就是最終被調(diào)用的那個(gè),實(shí)際上它是一個(gè)系統(tǒng)調(diào)用,而其它 5 個(gè)都是庫(kù)函數(shù)。上面就是 exec 函數(shù)族的主要關(guān)系,還有一些細(xì)節(jié)需要注意,下面分別說(shuō)明。
路徑搜索
帶 p 后綴的函數(shù)在搜索 PATH 環(huán)境變量時(shí),會(huì)依據(jù)分號(hào)(:)分隔多個(gè)路徑字段,例如
> echo $PATH
/bin:/usr/bin:/usr/local/bin:.
包含了四個(gè)路徑,按順序分別是
- /bin
- /usr/bin
- /usr/local/bin
- 當(dāng)前目錄
其中當(dāng)前目錄的表示方式有多種,除了顯示指定點(diǎn)號(hào)外,還可以
- 放置在最前
PATH=:/bin:/usr/bin:/usr/local/bin
- 放置在最后
PATH=/bin:/usr/bin:/usr/local/bin:
- 放置在中間
PATH=/bin::/usr/bin:/usr/local/bin
當(dāng)然了,不同的位置搜索優(yōu)先級(jí)也不同,并且也不建議將當(dāng)前路徑放置在 PATH 環(huán)境變量中。
參數(shù)列表
帶 l 后綴的函數(shù),以空指針作為參數(shù)列表的結(jié)尾,像下面這個(gè)例子
if (execlp("echoall", "echoall", "test", (char *)0) < 0)
err_sys ("execlp error");
如果使用常數(shù) 0,必需使用 char* 進(jìn)行強(qiáng)制轉(zhuǎn)換,否則它將被解釋為整型參數(shù),在整型長(zhǎng)度與指針長(zhǎng)度不同的平臺(tái)上, exec 函數(shù)的實(shí)際參數(shù)將會(huì)出錯(cuò)。
帶 v 后綴的函數(shù),也需要保證數(shù)組以空指針結(jié)尾,無(wú)論是 argv 還是 envp,最終都會(huì)被新程序的 main 函數(shù)接收,所以要求與 main 函數(shù)參數(shù)相同 (參考《[apue] 進(jìn)程環(huán)境那些事兒》),它們的 man 手冊(cè)頁(yè)中也有明確說(shuō)明:
The execv(), execvp(), and execvpe() functions provide an array of pointers to null-terminated
strings that represent the argument list available to the new program. The first argument, by con‐
vention, should point to the filename associated with the file being executed. The array of pointers
must be terminated by a NULL pointer.
配合 execve 的 man 內(nèi)容閱讀:
argv is an array of argument strings passed to the new program. By convention, the first of these
strings should contain the filename associated with the file being executed. envp is an array of
strings, conventionally of the form key=value, which are passed as environment to the new program.
Both argv and envp must be terminated by a NULL pointer. The argument vector and environment can be
accessed by the called program's main function, when it is defined as:
int main(int argc, char *argv[], char *envp[])
像附錄 8 那樣沒(méi)有給 argv 參數(shù)以空指針結(jié)尾帶來(lái)的問(wèn)題就很好理解了。
參數(shù)列表中的第一個(gè)參數(shù)一般指定為程序文件名,但這只是一種慣例,并無(wú)任何強(qiáng)制校驗(yàn)。每個(gè)系統(tǒng)對(duì)命令行參數(shù)和環(huán)境變量參數(shù)的總長(zhǎng)度都有一個(gè)限制,通過(guò)sysconf(ARG_MAX)
可獲?。?/p>
> getconf ARG_MAX
2097152
POSIX 規(guī)定此值不得小于 4096,當(dāng)使用 shell 的文件名擴(kuò)充功能 (*) 產(chǎn)生一個(gè)文件列表時(shí),可能會(huì)超過(guò)這個(gè)限制從而被截?cái)?,為避免產(chǎn)生這種問(wèn)題,可借助 xargs 命令將長(zhǎng)參數(shù)拆分成幾部分傳遞,書上給了一個(gè)查找 man 手冊(cè)中所有的 getrlimit 的例子:
查看代碼
> zgrep getrlimit /usr/share/man/*/*.gz
/usr/share/man/man0p/sys_resource.h.0p.gz:for the \fIresource\fP argument of \fIgetrlimit\fP() and \fIsetrlimit\fP():
/usr/share/man/man0p/sys_resource.h.0p.gz:int getrlimit(int, struct rlimit *);
/usr/share/man/man0p/sys_resource.h.0p.gz:\fIgetrlimit\fP()
/usr/share/man/man1/g++.1.gz:\&\s-1RAM \s0>= 1GB. If \f(CW\*(C`getrlimit\*(C'\fR is available, the notion of \*(L"\s-1RAM\*(R"\s0 is
/usr/share/man/man1/gcc.1.gz:\&\s-1RAM \s0>= 1GB. If \f(CW\*(C`getrlimit\*(C'\fR is available, the notion of \*(L"\s-1RAM\*(R"\s0 is
/usr/share/man/man1/perl561delta.1.gz:offers the getrlimit/setrlimit interface that can be used to adjust
/usr/share/man/man1/perl56delta.1.gz:offers the getrlimit/setrlimit interface that can be used to adjust
/usr/share/man/man1/perlhpux.1.gz: truncate, getrlimit, setrlimit
/usr/share/man/man2/brk.2.gz:.BR getrlimit (2),
/usr/share/man/man2/execve.2.gz:.BR getrlimit (2))
/usr/share/man/man2/fcntl.2.gz:.BR getrlimit (2)
/usr/share/man/man2/getpriority.2.gz:.BR getrlimit (2)
/usr/share/man/man2/getrlimit.2.gz:.\" 2004-11-16 -- mtk: the getrlimit.2 page, which formally included
/usr/share/man/man2/getrlimit.2.gz:getrlimit, setrlimit, prlimit \- get/set resource limits
/usr/share/man/man2/getrlimit.2.gz:.BI "int getrlimit(int " resource ", struct rlimit *" rlim );
/usr/share/man/man2/getrlimit.2.gz:.BR getrlimit ()
/usr/share/man/man2/getrlimit.2.gz:.BR getrlimit ()
/usr/share/man/man2/getrlimit.2.gz:.BR getrlimit ().
/usr/share/man/man2/getrlimit.2.gz:.BR getrlimit ().
/usr/share/man/man2/getrlimit.2.gz:.BR getrlimit (),
/usr/share/man/man2/getrlimit.2.gz:.\" getrlimit() and setrlimit() that use prlimit() to work around
/usr/share/man/man2/getrusage.2.gz:.\" 2004-11-16 -- mtk: the getrlimit.2 page, which formerly included
/usr/share/man/man2/getrusage.2.gz:.\" history, etc., see getrlimit.2
/usr/share/man/man2/getrusage.2.gz:.BR getrlimit (2),
/usr/share/man/man2/madvise.2.gz:.BR getrlimit (2),
/usr/share/man/man2/mremap.2.gz:.BR getrlimit (2),
/usr/share/man/man2/prlimit.2.gz:.so man2/getrlimit.2
/usr/share/man/man2/quotactl.2.gz:.BR getrlimit (2),
/usr/share/man/man2/sched_setscheduler.2.gz:.BR getrlimit (2))
/usr/share/man/man2/sched_setscheduler.2.gz:.BR getrlimit (2)).
/usr/share/man/man2/sched_setscheduler.2.gz:.BR getrlimit (2)
/usr/share/man/man2/sched_setscheduler.2.gz:.BR getrlimit (2).
/usr/share/man/man2/setrlimit.2.gz:.so man2/getrlimit.2
/usr/share/man/man2/syscalls.2.gz:\fBgetrlimit\fP(2) 1.0
/usr/share/man/man2/syscalls.2.gz:\fBugetrlimit\fP(2) 2.4
/usr/share/man/man2/syscalls.2.gz:.BR getrlimit (2)
/usr/share/man/man2/syscalls.2.gz:.IR sys_old_getrlimit ()
/usr/share/man/man2/syscalls.2.gz:.IR __NR_getrlimit )
/usr/share/man/man2/syscalls.2.gz:.IR sys_getrlimit ()
/usr/share/man/man2/syscalls.2.gz:.IR __NR_ugetrlimit ).
/usr/share/man/man2/ugetrlimit.2.gz:.so man2/getrlimit.2
/usr/share/man/man3/getdtablesize.3.gz:.BR getrlimit (2);
/usr/share/man/man3/getdtablesize.3.gz:.BR getrlimit (2)
/usr/share/man/man3/getdtablesize.3.gz:.BR getrlimit (2),
/usr/share/man/man3/malloc.3.gz:.BR getrlimit (2)).
/usr/share/man/man3/pcrestack.3.gz: getrlimit(RLIMIT_STACK, &rlim);
/usr/share/man/man3/pcrestack.3.gz:This reads the current limits (soft and hard) using \fBgetrlimit()\fP, then
/usr/share/man/man3p/exec.3p.gz:\fIgetenv\fP(), \fIgetitimer\fP(), \fIgetrlimit\fP(), \fImmap\fP(),
/usr/share/man/man3p/fclose.3p.gz:\fIclose\fP(), \fIfopen\fP(), \fIgetrlimit\fP(), \fIulimit\fP(),
/usr/share/man/man3p/fflush.3p.gz:\fIgetrlimit\fP(), \fIulimit\fP(), the Base Definitions volume of
/usr/share/man/man3p/fputc.3p.gz:\fIferror\fP(), \fIfopen\fP(), \fIgetrlimit\fP(), \fIputc\fP(),
/usr/share/man/man3p/fseek.3p.gz:\fIgetrlimit\fP(), \fIlseek\fP(), \fIrewind\fP(), \fIulimit\fP(),
/usr/share/man/man3p/getrlimit.3p.gz:.\" getrlimit
/usr/share/man/man3p/getrlimit.3p.gz:getrlimit, setrlimit \- control maximum resource consumption
/usr/share/man/man3p/getrlimit.3p.gz:int getrlimit(int\fP \fIresource\fP\fB, struct rlimit *\fP\fIrlp\fP\fB);
/usr/share/man/man3p/getrlimit.3p.gz:The \fIgetrlimit\fP() function shall get, and the \fIsetrlimit\fP()
/usr/share/man/man3p/getrlimit.3p.gz:Each call to either \fIgetrlimit\fP() or \fIsetrlimit\fP() identifies
/usr/share/man/man3p/getrlimit.3p.gz:considered to be larger than any other limit value. If a call to \fIgetrlimit\fP()
/usr/share/man/man3p/getrlimit.3p.gz:When using the \fIgetrlimit\fP() function, if a resource limit can
/usr/share/man/man3p/getrlimit.3p.gz:is unspecified unless a previous call to \fIgetrlimit\fP()
/usr/share/man/man3p/getrlimit.3p.gz:Upon successful completion, \fIgetrlimit\fP() and \fIsetrlimit\fP()
/usr/share/man/man3p/getrlimit.3p.gz:The \fIgetrlimit\fP() and \fIsetrlimit\fP() functions shall fail if:
/usr/share/man/man3p/setrlimit.3p.gz:.so man3p/getrlimit.3p
/usr/share/man/man3/pthread_attr_setstacksize.3.gz:.BR getrlimit (2),
/usr/share/man/man3/pthread_create.3.gz:.BR getrlimit (2),
/usr/share/man/man3/pthread_getattr_np.3.gz:.BR getrlimit (2),
/usr/share/man/man3/pthread_setschedparam.3.gz:.BR getrlimit (2),
/usr/share/man/man3/pthread_setschedprio.3.gz:.BR getrlimit (2),
/usr/share/man/man3p/ulimit.3p.gz:\fIgetrlimit\fP(), \fIsetrlimit\fP(), \fIwrite\fP(), the Base Definitions
/usr/share/man/man3p/write.3p.gz:\fIchmod\fP(), \fIcreat\fP(), \fIdup\fP(), \fIfcntl\fP(), \fIgetrlimit\fP(),
/usr/share/man/man3/ulimit.3.gz:.BR getrlimit (2),
/usr/share/man/man3/ulimit.3.gz:.BR getrlimit (2),
/usr/share/man/man3/vlimit.3.gz:.so man2/getrlimit.2
/usr/share/man/man3/vlimit.3.gz:.\" getrlimit(2) briefly discusses vlimit(3), so point the user there.
/usr/share/man/man5/core.5.gz:.BR getrlimit (2)
/usr/share/man/man5/core.5.gz:.BR getrlimit (2)
/usr/share/man/man5/core.5.gz:.BR getrlimit (2),
/usr/share/man/man5/limits.conf.5.gz:\fBgetrlimit\fR(2)\fBgetrlimit\fR(3p)
/usr/share/man/man5/proc.5.gz:.BR getrlimit (2)).
/usr/share/man/man5/proc.5.gz:.BR getrlimit (2).
/usr/share/man/man5/proc.5.gz:.BR getrlimit (2)).
/usr/share/man/man5/proc.5.gz:.BR getrlimit (2))
/usr/share/man/man7/credentials.7.gz:.BR getrlimit (2);
/usr/share/man/man7/daemon.7.gz:\fBgetrlimit()\fR
/usr/share/man/man7/mq_overview.7.gz:.BR getrlimit (2).
/usr/share/man/man7/mq_overview.7.gz:.BR getrlimit (2),
/usr/share/man/man7/signal.7.gz:.BR getrlimit (2),
/usr/share/man/man7/time.7.gz:.BR getrlimit (2),
我做了兩點(diǎn)改進(jìn):
- 使用 zgrep 代替 grep 或 bzgrep 搜索 gz 壓縮文件中的內(nèi)容
- 使用 /usr/share/man/*/*.gz 代替 */* 過(guò)濾子目錄
實(shí)測(cè)沒(méi)有報(bào)錯(cuò),看起來(lái)是因?yàn)閿?shù)據(jù)量還不夠大:
$ find /usr/share/man/ -type f -name "*.gz" | wc
9509 9509 361540
總字節(jié)大小為 361540 仍小于限制值 2097152。不過(guò)還是改成下面的形式更安全:
> find /usr/share/man -type f -name "*.gz" | xargs zgrep getrlimit
xargs 會(huì)自動(dòng)切分參數(shù),確保它們不超過(guò)限制,分批“喂”給 zgrep,從而實(shí)現(xiàn)參數(shù)長(zhǎng)度限制的突破,不過(guò)這樣做的前提是作業(yè)可被切分為多個(gè)進(jìn)程,如果必需由單個(gè)進(jìn)程完成,就不能這樣搞了。
最后,exec 的環(huán)境變量與命令行參數(shù)有類似的地方:
- 必需以空指針結(jié)尾
- 有總長(zhǎng)度限制
也有不同之處,那就是不指定 envp 參數(shù)時(shí),也可以通過(guò)修改當(dāng)前進(jìn)程的環(huán)境變量,來(lái)影響子進(jìn)程中的環(huán)境變量,這主要是通過(guò) setenv、putenv 接口,關(guān)于這點(diǎn)請(qǐng)參考《[apue] 進(jìn)程環(huán)境那些事兒》中環(huán)境變量一節(jié)的說(shuō)明。
解釋器文件
如果為帶 p 后綴的 exec 指定的文件不是一個(gè)由鏈接器產(chǎn)生的可執(zhí)行文件,則將該文件當(dāng)作一個(gè)腳本文件處理,此時(shí)將嘗試調(diào)用腳本首行中記錄的解釋器,格式如下:
#! pathname [ optional-argument ]
對(duì)這種文件的識(shí)別是由內(nèi)核作為 exec 系統(tǒng)調(diào)用處理的一部分來(lái)完成的,pathname 通常是路徑名 (絕對(duì) & 相對(duì)),并不對(duì)它進(jìn)行路徑搜索。內(nèi)核使調(diào)用 exec 函數(shù)的進(jìn)程實(shí)際執(zhí)行的并不是 file 參數(shù)本身,而是腳本第一行中 pathname 所指定的解釋器,例如最常見(jiàn)的:
#!/bin/sh
相當(dāng)于調(diào)用 /bin/sh path/to/script
,其中 #! 之后的空格是可選的;如果沒(méi)有首行標(biāo)記,則默認(rèn)是 shell 腳本;若解釋器需要選項(xiàng)才能支持腳本文件,則需要帶上相應(yīng)的選項(xiàng) (optional-argument),例如:
#! /bin/awk -f
最終相當(dāng)于調(diào)用 /bin/awk -f path/to/script
。書上有個(gè)不錯(cuò)的例子拿來(lái)做個(gè)測(cè)試:
#! /bin/awk -f
BEGIN {
for (i =0; i<ARGC; i++)
printf "argv[%d]: %s\n", i, ARGV[i]
exit
}
用于打印所有傳遞到 awk 腳本中的命令行參數(shù),執(zhí)行之:
> ./echoall.awk file1 FILENAME2 f3
argv[0]: awk
argv[1]: file1
argv[2]: FILENAME2
argv[3]: f3
有以下發(fā)現(xiàn):
- 第一個(gè)參數(shù)是 awk 而不是 echoall.awk
- 沒(méi)有參數(shù) -f
和書上講的不同,懷疑是 awk 做了處理 (-f 明顯沒(méi)有傳遞到內(nèi)部的必要),改為自己寫 C 程序版 echoall 驗(yàn)證:
#include <stdio.h>
int main (int argc, char *argv[])
{
int i;
for (i=0; i<argc; ++ i)
printf ("argv[%d]: %s\n", i, argv[i]);
exit (0);
}
腳本也需要稍微改進(jìn)一下:
#! ./echoall -f
因?yàn)槌绦蛞呀?jīng)做了所有工作,這里腳本內(nèi)容反而只有首行解釋器定義,再次執(zhí)行:
> ./echoall.sh file1 FILENAME2 f3
argv[0]: ./echoall
argv[1]: -f
argv[2]: ./echoall.sh
argv[3]: file1
argv[4]: FILENAME2
argv[5]: f3
這回有了 -f 選項(xiàng),并且它會(huì)被編排到 exec 函數(shù)中 argv 參數(shù)列表之前。書上的例子是直接使用 execl 來(lái)模擬內(nèi)核處理解釋器文件的:
#include "../apue.h"
#include <sys/wait.h>
#include <limits.h>
int main (int argc, char *argv[])
{
pid_t pid;
char *exename = "echoall.sh";
char pwd[PATH_MAX] = { 0 };
getcwd(pwd, PATH_MAX);
if (argc > 1)
exename = argv[1];
strcat (pwd, "/");
strcat (pwd, exename);
if ((pid = fork ()) < 0)
err_sys ("fork error");
else if (pid == 0)
{
if (execl (pwd, exename, "file1", "FILENAME2", "f3", (char *)0) < 0)
err_sys ("execl error");
}
if (waitpid (pid, NULL, 0) < 0)
err_sys ("wait error");
exit (0);
}
輸出與上例完全一致:
> ./exec
argv[0]: ./echoall
argv[1]: -f
argv[2]: /ext/code/apue/08.chapter/echoall.sh
argv[3]: file1
argv[4]: FILENAME2
argv[5]: f3
有趣的是 optional-argument (-f) 之后的第一個(gè)參數(shù) (argv[2]),execl 使用的是 path 參數(shù) (pwd),而不是參數(shù)列表中的第一個(gè)參數(shù) (exename):這是因?yàn)?path 參數(shù)包含了比第一個(gè)參數(shù)更多的信息,或者說(shuō)第一個(gè)參數(shù)是人為指定的,可以傳入任意值,存在較大的隨意性,遠(yuǎn)不如 path 參數(shù)可靠。
再考查一下多個(gè) optional-argument 的場(chǎng)景:
#! ./echoall -f test foo bar
新的輸出看起來(lái)把他們當(dāng)作了一個(gè):
> ./echoall.sh
argv[0]: ./echoall
argv[1]: -f test foo bar
argv[2]: ./echoall.sh
最多只有一個(gè)解釋器參數(shù),這就意味著除了 -f,不能為 awk 指定更多的額外參數(shù),例如 -F 指定分隔符,這一點(diǎn)需要注意。
解釋器首行也有最大長(zhǎng)度限制,而且與命令行參數(shù)長(zhǎng)度限制不是一回事,以上面的腳本為例,設(shè)置一個(gè) 128 長(zhǎng)度的參數(shù):
#! ./echoall aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
實(shí)際輸出不到 128:
> ./echoall.sh
argv[0]: ./echoall
argv[1]: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
argv[2]: ./echoall.sh
經(jīng)查只有 115,算上前面的 #! ./echoall
才剛好 128,可見(jiàn)該限制是施加在整個(gè)首行上的,且就是 128 (CentOS)。
最后,解釋器文件只是一種優(yōu)化而非必需,因?yàn)槿魏蜗到y(tǒng)命令或程序,都可以放在 shell 里執(zhí)行,使用解釋器文件只是簡(jiǎn)化了這一過(guò)程、提高了創(chuàng)建進(jìn)程的效率,如果解釋器因種種原因不可用 (例如一個(gè) optional-argument 不夠用),還可以回退到 shell 腳本的“老路”上來(lái)~
close-on-exec
之前說(shuō)過(guò),exec 后進(jìn)程 ID 不會(huì)改變,除此之外,執(zhí)行新程序的進(jìn)程還保持了原進(jìn)程的以下特征:
- 進(jìn)程 ID 和父進(jìn)程 ID
- 實(shí)際用戶 ID、實(shí)際組 ID
- 附加組 ID
- 進(jìn)程組 ID
- 會(huì)話 ID
- 控制終端
- 鬧鐘剩余時(shí)間
- 當(dāng)前工作目錄
- 根目錄
- 文件模式創(chuàng)建屏蔽字
- 文件鎖
- 信號(hào)屏蔽和安排
- 未處理信號(hào)
- 資源限制
- tms_utime & time_stime & tms_cutime & tms_cstime (參考進(jìn)程時(shí)間一節(jié))
- ……
一般不會(huì)改變的還有打開(kāi)文件描述符,說(shuō)一般是因?yàn)楫?dāng)設(shè)置某些標(biāo)志位后,描述符將被 exec 關(guān)閉,這個(gè)標(biāo)志位就是 close-on-exec (FD_CLOEXEC)。如果設(shè)置了該標(biāo)志,新進(jìn)程中的 fd 將被關(guān)閉,否則保持不變,默認(rèn)不設(shè)置該標(biāo)志。下面是典型的通過(guò) fcntl 獲取和設(shè)置該標(biāo)志的代碼:
flag = fcntl (fd, F_GETFD);
printf ("fd(%d) flag: 0x%x, CLOSE_ON_EXEC: %d\n", fd, flag, flag & FD_CLOEXEC);
// set CLOSE_ON_EXEC
fcntl (fd, F_SETFD, flag & ~FD_CLOEXEC);
POSIX.1 明確要求在執(zhí)行 exec 后關(guān)閉打開(kāi)的目錄流,這通常是由 opendir 在內(nèi)部實(shí)現(xiàn)的,它會(huì)為對(duì)應(yīng)的描述符設(shè)置 close-on-exec 標(biāo)志。下面這個(gè)程序驗(yàn)證了這一點(diǎn),并想方設(shè)法讓目錄流可以跨 exec 傳遞:
#include "../apue.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
char *str = 0;
int main (int argc, char *argv[])
{
int fd = 0;
DIR *dir = 0;
int flag = 0;
if (argc > 1)
{
// child mode
// get file descriptor from args
fd = atol(argv[1]);
char *s = argv[2];
flag = fcntl (fd, F_GETFD);
printf ("dir fd(%d) flag: 0x%x, CLOSE_ON_EXEC: %d\n", fd, flag, flag & FD_CLOEXEC);
dir = fdopendir(fd);
printf ("recv dir %d, str %s (total %d)\n", fd, s, argc);
flag = fcntl (fd, F_GETFD);
printf ("dir fd(%d) flag: 0x%x, CLOSE_ON_EXEC: %d\n", fd, flag, flag & FD_CLOEXEC);
rewinddir(dir);
struct dirent *ent = readdir (dir);
printf ("read %p, %s\n", ent, ent ? ent->d_name : 0);
closedir (dir);
}
else
{
str = strdup ("hello world");
dir = opendir (".");
if (dir == NULL)
err_sys ("open .");
else
printf ("open . return %p\n", dir);
fd = dirfd (dir);
flag = fcntl (fd, F_GETFD);
printf ("dir fd(%d) flag: 0x%x, CLOSE_ON_EXEC: %d\n", fd, flag, flag & FD_CLOEXEC);
// restore CLOSE_ON_EXEC
fcntl (fd, F_SETFD, flag & ~FD_CLOEXEC);
flag = fcntl (fd, F_GETFD);
printf ("dir fd(%d) flag: 0x%x, CLOSE_ON_EXEC: %d\n", fd, flag, flag & FD_CLOEXEC);
pid_t pid = 0;
if ((pid = fork ()) < 0)
err_sys ("fork error");
else if (pid == 0)
{
char tmp[32] = { 0 };
sprintf (tmp, "%lu", (long)fd);
execlp ("./exec_open_dir", "./exec_open_dir"/*argv[0]*/, tmp, str, NULL);
err_sys ("execlp error");
}
else
{
printf ("fork and exec child %u\n", pid);
struct dirent *ent = readdir (dir);
printf ("read %p, %s\n", ent, ent ? ent->d_name : 0);
// closedir (dir);
}
}
exit (0);
}
做個(gè)簡(jiǎn)單說(shuō)明:
- 程序編譯為 exec_open_dir,它有兩種模式:
- 無(wú)參數(shù)時(shí)打開(kāi)當(dāng)前目錄流,展示并清除它的 close-on-exec 標(biāo)志,啟動(dòng)子進(jìn)程,exec 替換進(jìn)程為自身 (exec_open_dir) 并傳遞這個(gè)目錄流的底層 fd 作為參數(shù);父進(jìn)程遍歷目錄流第一個(gè)文件并退出
- 有參數(shù)時(shí)直接打開(kāi)傳遞的文件句柄為目錄流,在 fd 轉(zhuǎn)換為目錄流前后分別打印它的 close-on-exec 標(biāo)志,rewind 至開(kāi)始并遍歷第一個(gè)文件,關(guān)閉目錄流退出
- 不帶任何參數(shù)啟動(dòng)時(shí)進(jìn)入 else 條件啟動(dòng)子進(jìn)程,帶參數(shù)啟動(dòng)時(shí)進(jìn)入 if 條件;父進(jìn)程進(jìn)入 else 條件,子進(jìn)程進(jìn)入 if 條件
下面看下這個(gè)程序的運(yùn)行結(jié)果:
> ./exec_open_dir
open . return 0x1c60030
dir fd(3) flag: 0x1, CLOSE_ON_EXEC: 1
dir fd(3) flag: 0x0, CLOSE_ON_EXEC: 0
fork and exec child 13085
read 0x1c60060, exec.c
dir fd(3) flag: 0x0, CLOSE_ON_EXEC: 0
recv dir 3, str hello world (total 3)
dir fd(3) flag: 0x1, CLOSE_ON_EXEC: 1
read 0x1a7c040, exec.c
做個(gè)簡(jiǎn)單說(shuō)明:
- 1-5:為父進(jìn)程輸出,opendir 后自帶 close-on-exec,手動(dòng)清除了這個(gè)標(biāo)志,能正常遍歷并打印第一個(gè)文件名
- 6-9:為子進(jìn)程輸出,接收到的 fd 不帶 close-on-exec,fdopendir 后設(shè)置了這個(gè)標(biāo)志,rewind 后能正常遍歷并打印第一個(gè)文件名
這里印證了兩點(diǎn):
- opendir & fdopendir 自動(dòng)添加 close-on-exec 標(biāo)志來(lái)保證目錄流跨 exec 的關(guān)閉
- 手動(dòng)清除目錄流底層 fd 上的 close-on-exec 可以保證目錄的跨 exec 傳遞
不過(guò)需要注意的是,重新打開(kāi)的目錄流必需 rewind 才可以遍歷,否則什么也不輸出,即使父進(jìn)程沒(méi)有遍歷到目錄結(jié)尾。注意這里不能直接傳遞 DIR 指針,因?yàn)?exec 后整個(gè)進(jìn)程的堆、棧會(huì)被替換,前程序的指針變量引用的內(nèi)容會(huì)失效。
最后,對(duì)比下 exec 和 fork 前后保留的進(jìn)程信息:
進(jìn)程信息 | fork | exec |
進(jìn)程 ID 和父進(jìn)程 ID | 變 | 不變 |
實(shí)際用戶 ID 和實(shí)際組 ID | 不變 | 不變 |
有效用戶 ID 和有效組 ID | 不變 | 可變 (set-uid/set-gid) |
附加組 ID | 不變 | 不變 |
進(jìn)程組 ID | 不變 | 不變 |
會(huì)話 ID | 不變 | 不變 |
控制終端 | 不變 | 不變 |
鬧鐘 | 清除未處理的 | 保持剩余時(shí)間 |
當(dāng)前工作目錄 | 不變 | 不變 |
根目錄 | 不變 | 不變 |
文件模式創(chuàng)建屏蔽字 | 不變 | 不變 |
文件鎖 | 清除 | 不變 |
信號(hào)屏蔽和安排 | 不變 | 不變 |
未處理信號(hào) | 清除 | 不變 |
資源限制 | 不變 | 不變 |
新進(jìn)程時(shí)間 (tms_xxtime) | 清除 | 不變 |
環(huán)境 | 不變 | 可變 |
連接的共享存儲(chǔ)段 | 不變 | 清除 |
存儲(chǔ)映射 | 不變 | 清除 |
文件描述符 | 不變 | 關(guān)閉 (close-on-exec) |
... | ? | ? |
更改進(jìn)程用戶 ID 和組 ID
Unix 系統(tǒng)中,特權(quán)是基于用戶和用戶組的,如果需要攬權(quán)操作,一般是通過(guò)切換啟動(dòng)進(jìn)程的用戶身份來(lái)實(shí)現(xiàn)的,例如 su 或 sudo。
然而一些需要訪問(wèn)特權(quán)文件的程序又需要對(duì)普通用戶開(kāi)放使用權(quán)限,例如 passwd,它修改的是普通用戶的賬戶密碼,但需要修改的文件 /etc/passwd 卻只有 root 才有寫權(quán)限,為此引入了 set-uid、set-gid 權(quán)限位標(biāo)識(shí)。當(dāng)普通啟動(dòng)具有 root 身份的 passwd 命令時(shí),新進(jìn)程將借用命令所有者的身份 (root) 而不是啟動(dòng)用戶的身份 (普通用戶),從而讓 passwd 命令可以寫 /etc/passwd 文件,實(shí)現(xiàn)讓普通用戶繞開(kāi)權(quán)限檢查的目的。
能這樣做的前提是 passwd 這個(gè)程序功能單一,內(nèi)部會(huì)對(duì)用戶身份進(jìn)行校驗(yàn),確保不會(huì)修改其它用戶的密碼,也不會(huì)做修改密碼以外的事情。Unix 系統(tǒng)對(duì)此有專門的保護(hù)機(jī)制,當(dāng) set-uid 或 set-gid 程序文件內(nèi)容發(fā)生變更時(shí),會(huì)自動(dòng)去除其 set-uid 和 set-gid 標(biāo)志位,確保程序沒(méi)有被黑客篡改來(lái)做一些非法的事情。除 passwd 外,類似的程序還有 crontab、at 等等,通過(guò)下面的命令可以查看系統(tǒng)中所有 set-uid 程序:
> find / -perm -u+s 2>/dev/null | xargs ls -lhd
關(guān)于用戶權(quán)限更詳細(xì)的內(nèi)容,可參考《[apue] linux 文件訪問(wèn)權(quán)限那些事兒》。
在解釋 set-uid、set-gid 機(jī)制之前先了解幾個(gè)術(shù)語(yǔ),進(jìn)程真實(shí)的用戶 ID 和用戶組 ID 稱為 RUID 和 RGID (real),這個(gè)一般是不變的;而權(quán)限檢查針對(duì)的是進(jìn)程的有效用戶 ID 與有效用戶組 ID,稱為 EUID 和 EGID (effect),默認(rèn)情況下 EUID = RUID、EGID = RGID,當(dāng)指定 set-uid 或 set-gid 標(biāo)志位時(shí),exec 會(huì)自動(dòng)將 EUID 或 EGID 設(shè)置為文件所屬的用戶 ID 與用戶組 ID,從而實(shí)現(xiàn)攬權(quán)的目的。這也是將本節(jié)安排在 exec 函數(shù)族之后的原因。
單有 set-uid、set-gid 標(biāo)志位還是不夠,考查一種命令的使用場(chǎng)景,它既要訪問(wèn)特權(quán)文件,還要啟動(dòng)子進(jìn)程,如果以特權(quán)身份啟動(dòng)子進(jìn)程,則存在權(quán)限濫用的問(wèn)題。為此,Unix 允許進(jìn)程自己控制 EUID、EGID 的變更,當(dāng)訪問(wèn)特權(quán)文件時(shí),使用特權(quán)身份訪問(wèn);當(dāng)啟動(dòng)子進(jìn)程時(shí),使用普通用戶身份啟動(dòng),從而滿足“最小化使用特權(quán)”的原則。
當(dāng)然了,EUID 與 EGID 不能隨意變更,否則會(huì)形成更大的安全漏洞,一般也就是在 RUID、RGID 與 set-uid、set-gid 指定的用戶身份之間切換,后面這組 ID 在切換后會(huì)丟失,需要將它們保存起來(lái),為此引入了新的術(shù)語(yǔ):saved-set-uid & saved-set-gid,簡(jiǎn)稱為 SUID 和 SGID,用來(lái)保存特權(quán)用戶身份,方便之后從這里恢復(fù)。
在早期 POSIX.1 標(biāo)準(zhǔn)中 SUID & SGID 是可選的,到 2001 版中才變?yōu)楸匦?,因此一些較老的系統(tǒng)可能不支持,程序中可以下面的代碼做編譯期測(cè)試:
#ifdef _POSIX_SAVED_IDS
printf ("support SUID & SGID!\n")
#else
printf ("NOT support SUID & SGID!\n")
#if
或通過(guò)下面的代碼在運(yùn)行期進(jìn)行驗(yàn)證:
if (sysconf (_SC_SAVED_IDS) == 1)
printf ("support SUID & SGID!\n");
else
printf ("NOT support SUID & SGID\n");
甚至支持命令行:
> getconf SAVED_IDS
1
> getconf _POSIX_SAVED_IDS
1
目前流行的大多數(shù)系統(tǒng)均支持這一特性。
?
上圖展示了到目前為止進(jìn)程內(nèi)部與權(quán)限相關(guān)的各種 ID,其中 SUID & SGID 沒(méi)有接口可以直接獲取,標(biāo)識(shí)為單獨(dú)的顏色。Linux 中有額外的擴(kuò)展接口可以獲取 SUID & SGID,所以可以通過(guò) ps 命令展示它們:
$ ps -efo ruid,euid,suid,rgid,egid,sgid,pid,ppid,cmd
RUID EUID SUID RGID EGID SGID PID PPID CMD
383278 383278 383278 100000 100000 100000 24537 24536 bash -c /usr/bin/baas login
383278 383278 383278 100000 100000 100000 24610 24537 \_ /bin/bash -l XDG_SESSION_ID=393387 TERM=xterm SHELL=/bin/bash
383278 383278 383278 100000 100000 100000 19001 24610 \_ ps -efo ruid,euid,suid,rgid,egid,sgid,pid,ppid,cmd
有了這個(gè)基礎(chǔ),可以將之前所說(shuō)的復(fù)雜權(quán)限控制場(chǎng)景通過(guò)下圖直觀展示出來(lái):
重點(diǎn)看下進(jìn)程的各個(gè) ID 是如何變更的:
- 進(jìn)程 100 以用戶身份 foo 通過(guò) fork + exec 啟動(dòng)了一個(gè) set-uid 程序,設(shè)置的用戶身份是 bar
- 啟動(dòng)后的進(jìn)程為 101,它的 EUID 為 bar 所以可以直接訪問(wèn)具有 bar 權(quán)限的文件
- 進(jìn)程 101 通過(guò) fork 啟動(dòng)了一個(gè)子進(jìn)程 102,它的用戶身份完全與 101 一致
- 進(jìn)程 102 在 exec 之前調(diào)整自己的用戶身份為 foo
- 進(jìn)程 102 在 exec 之后,完全丟失了 bar 的身份信息,沒(méi)有機(jī)會(huì)再轉(zhuǎn)換身份為 bar,從而達(dá)成了解除特權(quán)身份的目標(biāo)
一番操作猛如虎,具有特權(quán)身份的進(jìn)程 (101) 創(chuàng)建了一個(gè)普通身份的子進(jìn)程 (102),它是完完全全的普通身份,不像其父進(jìn)程一樣可以自由地在特權(quán)與普通身份之間切換,如同被閹割了一般。能這樣做其實(shí)隱藏了一條非常重要的規(guī)則:SUID & SGID 在 exec 時(shí),僅從 EUID & EGID 復(fù)制,如果 EUID & EGID 是由 exec 從 set-uid & set-gid 設(shè)置的,那么復(fù)制發(fā)生在它們被設(shè)置之后。這一點(diǎn)保證了,102 進(jìn)程在 exec 之前 SUID 為 bar,exec 之后它被同步為 foo;也是進(jìn)程 101 從 set-uid 程序創(chuàng)建時(shí)能記錄特權(quán)身份 (SUID 為 bar) 的關(guān)鍵。不得不說(shuō)這里的設(shè)計(jì)確實(shí)巧妙。創(chuàng)建子進(jìn)程只是一個(gè)例子,實(shí)際上可以是任意需要普通用戶權(quán)限的場(chǎng)景,因此這個(gè)圖還可以繼續(xù)擴(kuò)展,進(jìn)程 102 可以不斷在的特權(quán)用戶 (bar) 和啟動(dòng)用戶 (foo) 身份之間切換。
有了上面的鋪墊,再來(lái)看 Unix 提供的接口:
// user ID
uid_t getuid(void);
uid_t geteuid(void);
int setuid(uid_t uid);
int seteuid(uid_t euid);
int setreuid(uid_t ruid, uid_t euid);
// groupd ID
gid_t getgid(void);
gid_t getegid(void);
int setgid(gid_t gid);
int setegid(gid_t egid);
int setregid(gid_t rgid, gid_t egid);
4 個(gè) get 接口就不多解釋了,剩下的 6 個(gè) set 接口中,僅對(duì) 3 個(gè)設(shè)置 uid 的接口做個(gè)說(shuō)明。另外 3 個(gè)設(shè)置 gid 的接口情況類似,需要注意的是它們對(duì)進(jìn)程附加組 ID 沒(méi)有任何影響,關(guān)于后者,參考《[apue] linux 文件訪問(wèn)權(quán)限那些事兒》。
setuid
- root 進(jìn)程:RUID/EUID/SUID = uid
- 普通進(jìn)程
- uid == RUID:EUID = uid
- uid == SUID:EUID = uid
- 否則出錯(cuò)返回 -1,errno 設(shè)置為 EPERM
注意當(dāng)進(jìn)程本身為超級(jí)用戶進(jìn)程時(shí) (root),才可以更改 RUID,在系統(tǒng)中,通常由 login 程序在用戶登錄時(shí)調(diào)用 setuid 設(shè)置新進(jìn)程為當(dāng)前登錄用戶,而 login 確實(shí)就是一個(gè)超級(jí)用戶進(jìn)程。
非 root 進(jìn)程僅能將 EUID 設(shè)置為自己的 RUID 或 SUID,當(dāng)進(jìn)程不是 set-uid 進(jìn)程時(shí) (RUID = EUID = SUID),實(shí)際上調(diào)用這個(gè)接口沒(méi)有意義,因?yàn)椴荒軐?EUID 更改為其它值。
seteuid
- root 進(jìn)程:EUID = uid
- 普通進(jìn)程
- uid == RUID:EUID = uid
- uid == SUID:EUID = uid
- 否則出錯(cuò)返回 -1,errno 設(shè)置為 EPERM
這個(gè)接口對(duì)于普通進(jìn)程而言,與 setuid 無(wú)異;對(duì)于超級(jí)用戶進(jìn)程而言,唯一的區(qū)別是只設(shè)置 EUID,保持 RUID 與 SUID 不變。
setreuid
- root 進(jìn)程:RUID = ruid;EUID = euid
- 普通進(jìn)程
- ruid
- -1:RUID 不變
- ruid == EUID:RUID = ruid
- ruid == SUID:RUID = ruid
- 否則出錯(cuò)返回 -1,errno 設(shè)置為 EPERM
- euid
- -1:EUID 不變
- euid == RUID:EUID = euid
- euid == SUID:EUID = euid
- 否則出錯(cuò)返回 -1,errno 設(shè)置為 EPERM
- ruid
這個(gè)接口來(lái)源于 SUS 標(biāo)準(zhǔn),最早是 BSD 4.3 引入的,由于當(dāng)時(shí)沒(méi)有 saved-set-uid 機(jī)制,只能通過(guò)交換 RUID 與 EUID 的方法來(lái)實(shí)現(xiàn)特權(quán)與普通用戶身份的切換。隨著與 saved-set-uid 機(jī)制的整合,相應(yīng)的判斷條件也增加了一個(gè) (item III):可以把 RUID 或 EUID 設(shè)置為 SUID。setreuid (-1, uid) 等價(jià)于 seteuid,另外 setreuid 還能實(shí)現(xiàn)普通進(jìn)程 RUID 的變更,這是之前接口沒(méi)有的能力。
demo
下面的程序用來(lái)驗(yàn)證:
#include "../apue.h"
#include <sys/types.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <unistd.h>
void print_ids ()
{
uid_t ruid = 0;
uid_t euid = 0;
uid_t suid = 0;
int ret = getresuid (&ruid, &euid, &suid);
if (ret == 0)
printf ("%d: ruid %d, euid %d, suid %d\n", getpid(), ruid, euid, suid);
else
err_sys ("getresuid");
}
int main (int argc, char *argv[])
{
if (argc == 2)
{
char* uid=argv[1];
int ret = setuid(atol(uid));
if (ret != 0)
err_sys ("setuid");
print_ids();
}
else if (argc == 3)
{
char* ruid=argv[1];
char* euid=argv[2];
int ret = setreuid(atol(ruid), atol(euid));
if (ret != 0)
err_sys ("setreuid");
print_ids();
}
else if (argc > 1)
{
char* uid=argv[1];
int ret = seteuid(atol(uid));
if (ret != 0)
err_sys ("seteuid");
print_ids();
}
else
{
print_ids();
}
return 0;
}
對(duì) demo 的參數(shù)做個(gè)簡(jiǎn)單說(shuō)明:
- 1 個(gè)參數(shù):調(diào)用 setuid,argv[1] 為 uid,整型
- 2 個(gè)參數(shù):調(diào)用 setreuid,argv[1] 為 ruid,argv[2] 為 euid,整型
- >2 個(gè)參數(shù):調(diào)用 seteuid,argv[1] 為 euid,整型,其它隨意,僅用于占位
- 無(wú)參數(shù):打印當(dāng)前進(jìn)程 RUID / EUID / SUID
變更后也會(huì)打印當(dāng)前進(jìn)程 RUID / EUID / SUID。這里為了直觀起見(jiàn),使用了 Linux 上獨(dú)有的 getresuid 接口,缺點(diǎn)是犧牲了可移植性。下面是驅(qū)動(dòng)腳本:
#!/bin/sh
groupadd test
echo "create group ok"
useradd -g test foo
useradd -g test bar
foo_uid=$(id -u foo)
bar_uid=$(id -u bar)
echo "create user ok"
echo " foo: ${foo_uid}"
echo " bar: ${bar_uid}"
cd /tmp
chown bar:test ./setuid
echo "test foo"
su foo -c ./setuid
chmod u+s ./setuid
echo "test set-uid bar"
su foo -c ./setuid
echo "test setuid(foo)"
su foo -c "./setuid ${foo_uid}"
echo "test seteuid(foo)"
su foo -c "./setuid ${foo_uid} noop noop"
echo "test setreuid(bar, foo)"
su foo -c "./setuid ${bar_uid} ${foo_uid}"
echo "test setreuid(-1, foo)"
su foo -c "./setuid -1 ${foo_uid}"
echo "test setreuid(bar, -1)"
su foo -c "./setuid ${bar_uid} -1"
userdel bar
userdel foo
echo "remove user ok"
rm -rf /home/bar
rm -rf /home/foo
echo "remove user home ok"
groupdel test
echo "delete group ok"
這個(gè)腳本制造了不同的條件來(lái)調(diào)用上面的 setuid 程序,前提是將 demo 事先放置在 /tmp 目錄。運(yùn)行后產(chǎn)生下面的輸出:
> sudo sh setuid.sh
create group ok
create user ok
foo: 1003
bar: 1004
test foo
4549: ruid 1003, euid 1003, suid 1003
test set-uid bar
4562: ruid 1003, euid 1004, suid 1004
test setuid(foo)
4574: ruid 1003, euid 1003, suid 1004
test seteuid(foo)
4586: ruid 1003, euid 1003, suid 1004
test setreuid(bar, foo)
4598: ruid 1004, euid 1003, suid 1003
test setreuid(-1, foo)
4617: ruid 1003, euid 1003, suid 1004
test setreuid(bar, -1)
4629: ruid 1004, euid 1004, suid 1004
remove user ok
remove user home ok
delete group ok
腳本構(gòu)造了測(cè)試所需的所有賬戶,包括一個(gè)用戶組 test,兩個(gè)測(cè)試賬號(hào) foo(1003) 與 bar(1004),測(cè)試結(jié)束后又自動(dòng)清理了這些賬戶。分別驗(yàn)證了以下場(chǎng)景:
- 未 set-uid:RUID (foo),EUID (foo),SUID (foo)
- set-uid bar
- 空參數(shù):RUID (foo),EUID (bar),SUID (bar)
- setuid (foo):RUID (foo),EUID (foo),SUID (bar)
- seteuid (foo):RUID (foo),EUID (foo),SUID (bar)
- setreuid (bar, foo):RUID (bar),EUID (foo),SUID (bar)
- setreuid (-1,foo):RUID (foo),EUID (foo),SUID (bar)
- setreuid (bar, -1):RUID (bar),EUID (bar),SUID (bar)
都是以 foo 身份啟動(dòng)的,主要看 set-uid 為 bar 的場(chǎng)景:
- setuid、seteuid 與
setreuid(-1,foo)
在這個(gè)場(chǎng)景等價(jià) - setreuid 可以改變 RUID 的值,setreuid (bar,-1) 甚至允許用戶永久拋棄普通用戶身份,"理直氣壯"的作個(gè)特權(quán)進(jìn)程
對(duì)于上面最后一個(gè)用例,三個(gè) ID 都變更為了 bar,有人可能會(huì)問(wèn)了,此時(shí)進(jìn)程還能恢復(fù) foo 的身份嗎?在 print_ids 中增加一小段代碼做個(gè)驗(yàn)證:
int ret = getresuid (&ruid, &euid, &suid);
if (ret == 0)
{
if (ouid != -1)
{
printf ("%d: ruid %d, euid %d, suid %d, ouid %d\n", getpid(), ruid, euid, suid, ouid);
if (ruid == euid && euid == suid && suid != ouid)
{
printf ("all uid same %d, change back to old %d\n", ruid, ouid);
ret = seteuid (ouid);
if (ret != 0)
err_sys ("seteuid");
else
print_ids (0);
}
}
else
printf ("%d: ruid %d, euid %d, suid %d\n", getpid(), ruid, euid, suid);
}
else
err_sys ("getresuid");
主要邏輯就是:判斷三個(gè) ID 相等后,嘗試 change back 到之前的普通用戶身份,原來(lái)的身份由 ouid 記錄并經(jīng)外層傳入,這里是在調(diào)用 setreuid 之前使用 getuid 備份了之前的值:
else if (argc == 3)
{
char* ruid=argv[1];
char* euid=argv[1];
uid_t ouid = getuid();
int ret = setreuid(atol(ruid), atol(euid));
if (ret != 0)
err_sys ("setreuid");
// to test if ruid/euid/suid changed to same
// can we change back again?
print_ids(ouid);
}
其他場(chǎng)景直接傳 -1 即可。重新運(yùn)行上面的腳本:
> sudo sh setuid.sh
create group ok
create user ok
foo: 1003
bar: 1004
test foo
4549: ruid 1003, euid 1003, suid 1003
test set-uid bar
4562: ruid 1003, euid 1004, suid 1004
test setuid(foo)
4574: ruid 1003, euid 1003, suid 1004
test seteuid(foo)
4586: ruid 1003, euid 1003, suid 1004
test setreuid(bar, foo)
4598: ruid 1004, euid 1003, suid 1003, ouid 1003
test setreuid(-1, foo)
4617: ruid 1003, euid 1003, suid 1004, ouid 1003
test setreuid(bar, -1)
4629: ruid 1004, euid 1004, suid 1004, ouid 1003
all uid same 1004, change back to old 1003
seteuid: Operation not permitted
remove user ok
remove user home ok
delete group ok
果然失敗了 (EPERM),這從另一個(gè)角度驗(yàn)證了之前的約束:seteuid 只能將 EUID 更新為 RUID 或 SUID 之一。在 setreuid(-1,foo)
的場(chǎng)景中,RUID = EUID = foo,僅 SUID = bar,此時(shí)切換到 bar 應(yīng)該可行,感興趣的讀者可以一試。
root demo1
demo 中的 bar 并不是超級(jí)用戶,而 set-uid 的大多數(shù)場(chǎng)景是超級(jí)用戶,將 bar 切換為 root 會(huì)有什么不同?將原始腳本中的 bar 都改為 root (且去掉創(chuàng)建、刪除 root 賬戶的代碼) 再試:
> sudo sh setuid-setroot.sh
create group ok
create user ok
foo: 1003
root: 0
test foo
16370: ruid 1003, euid 1003, suid 1003
test set-uid root
16383: ruid 1003, euid 0, suid 0
test setuid(foo)
16395: ruid 1003, euid 1003, suid 1003
test seteuid(foo)
16408: ruid 1003, euid 1003, suid 0
test setreuid(root, foo)
16420: ruid 0, euid 1003, suid 1003
test setreuid(-1, foo)
16432: ruid 1003, euid 1003, suid 0
test setreuid(root, -1)
16445: ruid 0, euid 0, suid 0
remove user ok
remove user home ok
delete group ok
除了以下不同外外,其它沒(méi)區(qū)別:
- setuid 會(huì)將 3 個(gè) ID 設(shè)置為一樣
- setreuid 后 SUID 也將會(huì)被變更為新的 EUID
后一條在 man 中找到了解釋:
If the real user ID is set or the effective user ID is set to a value not equal to the previous real user ID, the saved set-user-ID will be set to the new effective user ID.
意思是無(wú)論 EUID 還是 RUID,只要與之前的 RUID 不同,SUID 都會(huì)隨之變更。關(guān)于 SUID 的變更,可以參考下一小節(jié)的例子,現(xiàn)在接著上一個(gè)例子的熱度,再驗(yàn)證下 ID 一樣的情況下是否還有 change back 的能力:
> sudo sh setuid-setroot.sh
create group ok
create user ok
foo: 1003
root: 0
test foo
5842: ruid 1003, euid 1003, suid 1003
test set-uid root
5855: ruid 1003, euid 0, suid 0
test setuid(foo)
5873: ruid 1003, euid 1003, suid 1003, ouid 0
all uid same 1003, change back to old 0
seteuid: Operation not permitted
test seteuid(foo)
5885: ruid 1003, euid 1003, suid 0
test setreuid(root, foo)
5897: ruid 0, euid 1003, suid 1003, ouid 1003
test setreuid(-1, foo)
5909: ruid 1003, euid 1003, suid 0, ouid 1003
test setreuid(root, -1)
5921: ruid 0, euid 0, suid 0, ouid 1003
all uid same 0, change back to old 1003
5921: ruid 0, euid 1003, suid 0
remove user ok
remove user home ok
delete group ok
如果已變身為普通用戶,不能 change back;如果是超級(jí)用戶,可以。
root demo2
上個(gè)例子中,超級(jí)用戶進(jìn)程在變更 EUID 時(shí) SUID 會(huì)隨之變更,然而 man 中說(shuō) RUID 變更時(shí) SUID 才會(huì)隨之變更,為了看的更清楚些,寫了一個(gè) setreuid 的測(cè)試腳本:
#!/bin/sh
groupadd test
echo "create group ok"
useradd -g test foo
useradd -g test bar
foo_uid=$(id -u foo)
bar_uid=$(id -u bar)
echo "create user ok"
echo " foo: ${foo_uid}"
echo " bar: ${bar_uid}"
cd /tmp
#chown bar:test ./setuid
echo "test foo"
./setuid
#chmod u+s ./setuid
#echo "test set-uid bar"
#su foo -c ./setuid
echo "test setreuid(bar, foo)"
./setuid ${bar_uid} ${foo_uid}
echo "test setreuid(foo, bar)"
./setuid ${foo_uid} ${bar_uid}
echo "test setreuid(-1, foo)"
./setuid -1 ${foo_uid}
echo "test setreuid(bar, -1)"
./setuid ${bar_uid} -1
echo "test setreuid(bar, bar)"
./setuid ${bar_uid} ${bar_uid}
echo "test setreuid(foo, foo)"
./setuid ${foo_uid} ${foo_uid}
userdel bar
userdel foo
echo "remove user ok"
rm -rf /home/bar
rm -rf /home/foo
echo "remove user home ok"
groupdel test
echo "delete group ok"
仍然創(chuàng)建 foo、bar 賬戶,不同的是直接使用超級(jí)用戶身份啟動(dòng) setuid,并傳遞不同的 foo、bar 參數(shù)給 setreuid 進(jìn)行測(cè)試:
> sudo sh setreuid.sh
create group ok
create user ok
foo: 1003
bar: 1004
test foo
27253: ruid 0, euid 0, suid 0
test setreuid(bar, foo)
27254: ruid 1004, euid 1003, suid 1003
test setreuid(foo, bar)
27255: ruid 1003, euid 1004, suid 1004
test setreuid(-1, foo)
27256: ruid 0, euid 1003, suid 1003
test setreuid(bar, -1)
27257: ruid 1004, euid 0, suid 0
test setreuid(bar, bar)
27258: ruid 1004, euid 1004, suid 1004
test setreuid(foo, foo)
27259: ruid 1003, euid 1003, suid 1003
remove user ok
remove user home ok
測(cè)試了 6 個(gè)場(chǎng)景,這下看清楚了,SUID 變更基本與 EUID 是同步的,而 RUID 的變更對(duì) SUID 反而沒(méi)有什么影響。
需要注意的是,與 demo2 的 setreuid(-1,foo)
場(chǎng)景不同,demo1 的 SUID 仍保持 0 而不是變更為 1003,這里有點(diǎn)說(shuō)不通,兩個(gè)例子唯一的區(qū)別僅是獲取的超級(jí)用戶權(quán)限的途徑,demo1 通過(guò) set-uid root;demo2 通過(guò)啟動(dòng)用戶本身是 root。為 demo1 增加 setreuid(foo,bar)
與 setreuid(bar,foo)
兩個(gè)場(chǎng)景做對(duì)比,新的輸出如下:
> sudo sh setuid-setroot.sh
create group ok
create user ok
foo: 1003
root: 0
test foo
7475: ruid 1003, euid 1003, suid 1003
test set-uid root
7488: ruid 1003, euid 0, suid 0
test setuid(foo)
7500: ruid 1003, euid 1003, suid 1003
test seteuid(foo)
7512: ruid 1003, euid 1003, suid 0
test setreuid(root, foo)
7524: ruid 0, euid 1003, suid 1003
test setreuid(-1, foo)
7536: ruid 1003, euid 1003, suid 0
test setreuid(root, -1)
7548: ruid 0, euid 0, suid 0
test setreuid(foo, bar)
7560: ruid 1003, euid 1003, suid 1003
test setreuid(bar, foo)
7572: ruid 1003, euid 1003, suid 1003
remove user ok
remove user home ok
delete group ok
神奇的事情發(fā)生了,雖然理論上現(xiàn)在進(jìn)程擁有特權(quán),然而卻不能設(shè)置 foo & root 之外的用戶身份,這兩個(gè)用例最終都變成了 foo,看起來(lái)借用的特權(quán)和原生的還是有很大差別。
好奇 setuid 和 seteuid 的表現(xiàn)如何,添加下面的用例:
echo "test setuid(foo)"
su foo -c "./setuid ${foo_uid}"
echo "test setuid(bar)"
su foo -c "./setuid ${bar_uid}"
echo "test seteuid(foo)"
su foo -c "./setuid ${foo_uid} noop noop"
echo "test seteuid(bar)"
su foo -c "./setuid ${bar_uid} noop noop"
主要驗(yàn)證 setuid(bar)
& seteuid(bar)
的情況:
test setuid(foo)
27292: ruid 1003, euid 1003, suid 1003
test setuid(bar)
27304: ruid 1003, euid 0, suid 0
test seteuid(foo)
27316: ruid 1003, euid 1003, suid 0
test seteuid(bar)
27328: ruid 0, euid 0, suid 0
更離譜的情況出現(xiàn)了,setuid(bar)
不生效也不報(bào)錯(cuò);seteuid(bar)
更是直接回退到了 root。感覺(jué) set-uid root 的進(jìn)程邏輯有點(diǎn)混亂。
雖然不清楚 Linux 底層是如何處理的,但是大膽假設(shè)一下,這里的邏輯應(yīng)該和 RUID 相關(guān):當(dāng)以 root 身份啟動(dòng)時(shí),RUID = EUID = 0;而以 set-uid root 身份啟動(dòng)時(shí),RUID != 0。然而可以人為將 set-uid root 的 RUID 修改為 0 (通過(guò) setreuid(root, -1)
實(shí)現(xiàn)),此時(shí)它滿足 RUID = EUID = 0 的條件,再執(zhí)行 setreuid(foo,bar)
還能成功嗎?修改 setuid.c 程序進(jìn)行驗(yàn)證:
uid_t ruid = 0;
uid_t euid = 0;
uid_t suid = 0;
int ret = getresuid (&ruid, &euid, &suid);
if (ret == 0)
{
printf ("%d: ruid %d, euid %d, suid %d\n", getpid(), ruid, euid, suid);
if (ruid == 0 && euid == 0)
{
// in root, try setreuid(foo, bar)
int ret = setreuid(1003, 1004);
if (ret != 0)
err_sys ("setreuid");
else
print_ids (-1);
}
}
else
err_sys ("getresuid");
當(dāng)檢測(cè)到全 root ID 時(shí),在 print_ids 中再調(diào)用一次 setreuid,這里為方便直接寫死了 foo、bar 的用戶 ID (1003/1004)。重新運(yùn)行上面的腳本:
> sudo sh setuid-setroot.sh
create group ok
create user ok
foo: 1003
root: 0
test foo
3767: ruid 1003, euid 1003, suid 1003
test set-uid root
3780: ruid 1003, euid 0, suid 0
test setuid(foo)
3792: ruid 1003, euid 1003, suid 1003
test setuid(bar)
3804: ruid 1003, euid 0, suid 0
test seteuid(foo)
3817: ruid 1003, euid 1003, suid 0
test seteuid(bar)
3829: ruid 0, euid 0, suid 0
3829: ruid 1003, euid 1004, suid 1004
test setreuid(root, foo)
3841: ruid 0, euid 1003, suid 1003
test setreuid(-1, foo)
3853: ruid 1003, euid 1003, suid 0
test setreuid(root, -1)
3865: ruid 0, euid 0, suid 0
3865: ruid 1003, euid 1004, suid 1004
test setreuid(foo, bar)
3878: ruid 1003, euid 1003, suid 1003
test setreuid(bar, foo)
3890: ruid 1003, euid 1003, suid 1003
remove user ok
remove user home ok
delete group ok
重點(diǎn)看 test setreuid(root,-1)
之后的輸出,全 root ID 后是可以正確設(shè)置 RUID = foo、EUID = bar 身份的,看來(lái)就是 RUID 與? EUID 不一致?lián)v的鬼!
總結(jié)一下,如果 EUID = 0 為超級(jí)用戶權(quán)限,那么在是否能隨意設(shè)置其它用戶身份這個(gè)問(wèn)題上,還要看 RUID 的值,如果 RUID = 0,可以;否則,有限制。而對(duì)于這個(gè)進(jìn)程是通過(guò) root 身份獲取的全 0 ID,還是通過(guò) set-uid root 再 setreuid 獲取的全 0 ID,系統(tǒng)并不 care。至于 Linux 源碼是不是這樣寫的,這個(gè)存疑,留待以后查看源碼再做結(jié)論。
root demo3
來(lái)看一個(gè)冷門但存在的場(chǎng)景:set-uid bar 但以超級(jí)用戶身份啟動(dòng)進(jìn)程。需要將原始腳本中 foo 替換為 root、所有 su foo -c
去掉? (且去掉創(chuàng)建、刪除賬戶的代碼) :
> sudo sh setuid-root.sh
create group ok
create user ok
root: 0
bar: 1003
test root
14996: ruid 0, euid 0, suid 0
test set-uid bar
14998: ruid 0, euid 1003, suid 1003
test setuid(root)
14999: ruid 0, euid 0, suid 1003
test seteuid(root)
15000: ruid 0, euid 0, suid 1003
test setreuid(bar, root)
15001: ruid 1003, euid 0, suid 0
test setreuid(-1, root)
15002: ruid 0, euid 0, suid 1003
test setreuid(bar, -1)
15003: ruid 1003, euid 1003, suid 1003
remove user ok
remove user home ok
delete group ok
這個(gè)反向 set-uid 能實(shí)現(xiàn)特權(quán)"降級(jí)"效果,其中較有有趣的是 setuid (root) 的場(chǎng)景,它僅設(shè)置 EUID 為 0,進(jìn)一步驗(yàn)證了進(jìn)程的特權(quán)是從 EUID 而來(lái) (老 EUID 為 bar 非特權(quán)用戶,雖然 RUID 為 0)。
mac demo
好奇 mac 上的表現(xiàn)是否一致?將原始程序移植到 mac 上 (去掉 SUID 的獲取和展示),直接啟動(dòng)腳本發(fā)現(xiàn)創(chuàng)建用戶和組的命令會(huì)報(bào)錯(cuò),在 mac 上缺少 groupadd、useradd 等命令,必需手動(dòng)創(chuàng)建它們:
將原始腳本中創(chuàng)建、刪除賬戶的代碼移除,直接基于上面創(chuàng)建好的用戶和組進(jìn)行測(cè)試:
> sudo sh setuid.sh
create user ok
foo: 501
bar: 502
test foo
30106: ruid 501, euid 501
test set-uid bar
30109: ruid 501, euid 502
test setuid(foo)
30111: ruid 501, euid 501
test seteuid(foo)
30113: ruid 501, euid 501
test setreuid(bar, foo)
30115: ruid 502, euid 501
test setreuid(-1, foo)
30117: ruid 501, euid 501
test setreuid(bar, -1)
30119: ruid 502, euid 502
雖然無(wú)法看到 SUID,表現(xiàn)卻和 Linux 一致。如法炮制,繼續(xù)驗(yàn)證 root demo1:
> sudo sh setuid-setroot.sh
create user ok
foo: 501
root: 0
test foo
2987: ruid 501, euid 501
test set-uid root
2990: ruid 501, euid 0
test setuid(foo)
2992: ruid 501, euid 501
test setuid(bar)
2994: ruid 501, euid 0
test seteuid(foo)
2996: ruid 501, euid 501
test seteuid(bar)
2998: ruid 0, euid 0
test setreuid(root, foo)
3000: ruid 0, euid 501
test setreuid(-1, foo)
3002: ruid 501, euid 501
test setreuid(root, -1)
3004: ruid 0, euid 0
test setreuid(foo, bar)
3006: ruid 501, euid 501
test setreuid(bar, foo)
3008: ruid 501, euid 501
驗(yàn)證 set-uid 為 root 的場(chǎng)景,并且融合了部分 root demo2 的場(chǎng)景,即在 set-uid root 獲取超級(jí)用戶權(quán)限的情況下,能否設(shè)置其它用戶身份,結(jié)果與 Linux 一致:不能。接下來(lái)驗(yàn)證 root demo2:
> sudo sh setreuid.sh
create user ok
foo: 501
bar: 502
test foo
3410: ruid 0, euid 0
test setreuid(bar, foo)
3411: ruid 502, euid 501
test setreuid(foo, bar)
3412: ruid 501, euid 502
test setreuid(-1, foo)
3413: ruid 0, euid 501
test setreuid(bar, -1)
3414: ruid 502, euid 0
test setreuid(bar, bar)
3415: ruid 502, euid 502
test setreuid(foo, foo)
3416: ruid 501, euid 501
以超級(jí)用戶啟動(dòng)進(jìn)程的情況下 setreuid 設(shè)置任意用戶的能力,與 Linux 也是一致的:能。最后驗(yàn)證 root demo3:
> sudo sh setuid-root.sh
create user ok
root: 0
bar: 502
test root
3679: ruid 0, euid 0
test set-uid bar
3681: ruid 0, euid 502
test setuid(root)
3682: ruid 0, euid 0
test seteuid(root)
3683: ruid 0, euid 0
test setreuid(bar, root)
3684: ruid 502, euid 0
test setreuid(-1, root)
3685: ruid 0, euid 0
test setreuid(bar, -1)
3686: ruid 502, euid 502
以超級(jí)用戶身份啟動(dòng) set-uid 普通用戶身份的進(jìn)程,結(jié)果也是與 Linux 一致的。
最終結(jié)論,mac 上的 setuid 函數(shù)族表現(xiàn)與 linux 完全一致,特別是在 set-uid root 獲取的超級(jí)用戶權(quán)限時(shí)的一些表現(xiàn),可以明確的一點(diǎn)就是這些異常 case 并不是 Linux 獨(dú)有的,而是廣泛分布于 Unix 系統(tǒng)。當(dāng)然由于在 mac 上看不到 SUID,關(guān)于 SUID 的部分不在本節(jié)討論范圍內(nèi)。
總結(jié)
結(jié)合之前對(duì) exec 的說(shuō)明,setuid 函數(shù)族對(duì)權(quán)限 ID 的影響可以歸納為一個(gè)表格:
啟動(dòng)身份 | set-uid 身份 | 接口 | RUID | EUID | SUID |
foo | n/a | n/a | foo | foo | foo |
root | n/a | foo | 0 | 0 | |
setuid (foo) | foo | foo | foo | ||
setuid (bar) | foo | 0 | 0 | ||
seteuid (foo) | foo | foo | 0 | ||
seteuid (bar) | 0 | 0 | 0 | ||
setreuid (root, foo) | 0 | foo | 0 | ||
setreuid (root, -1) | 0 | 0 | 0 | ||
setreuid (-1, foo) | foo | foo | 0 | ||
bar | n/a | foo | bar | bar | |
setuid (foo) | foo | foo | bar | ||
seteuid (foo) | foo | foo | bar | ||
setreuid (bar, foo) | bar | foo | bar | ||
root | n/a | n/a | 0 | 0 | 0 |
setuid (foo) | foo | foo | foo | ||
seteuid (foo) | 0 | foo | 0 | ||
setreuid (foo, bar) | foo | bar | bar | ||
setreuid (bar, foo) | bar | foo | foo | ||
bar | n/a | 0 | bar | bar | |
setuid (root) | 0 | 0 | bar | ||
seteuid (root) | 0 | 0 | bar | ||
setreuid (bar, root) | bar | 0 | bar |
其中 foo 和 bar 都是普通用戶,表中驗(yàn)證了之前討論的幾種場(chǎng)景:
- foo/no-set-uid:普通用戶啟動(dòng)普通進(jìn)程,只能訪問(wèn)自己的文件
- foo/set-uid root:普通用戶啟動(dòng)超級(jí)用戶進(jìn)程 (setuid 主要場(chǎng)景)
- foo/set-uid bar:普通用戶啟動(dòng)普通進(jìn)程,能訪問(wèn)另一個(gè)普通用戶的文件? (冷門場(chǎng)景但存在)
- root/no-set-uid:超級(jí)用戶啟動(dòng)超級(jí)用戶進(jìn)程
- root/set-uid bar:超級(jí)用戶啟動(dòng)普通進(jìn)程 (冷門場(chǎng)景幾乎不存在)
把這個(gè)表弄懂,Unix 上進(jìn)程權(quán)限變化就了然于胸了。
回顧
本節(jié)開(kāi)頭那個(gè)復(fù)雜的進(jìn)程特權(quán)控制的例子:
在切換 EUID 時(shí),理論上使用上面三個(gè)接口都可以,但經(jīng)過(guò)實(shí)測(cè):
- setuid 在 root 場(chǎng)景下會(huì)同時(shí)修改 3 個(gè) ID
- setreuid 場(chǎng)景復(fù)雜
- 對(duì) SUID 有說(shuō)不清楚的影響
- set-uid root 場(chǎng)景下可設(shè)置其它用戶身份而不報(bào)錯(cuò),但結(jié)果不符合預(yù)期
- 僅交換 RUID & EUID 并不能實(shí)現(xiàn)子進(jìn)程的特權(quán)回收,因?yàn)樽舆M(jìn)程可以通過(guò)繼續(xù)調(diào)用 setreuid 恢復(fù)特權(quán),如果將 ruid 參數(shù)設(shè)置為 -1,則退化為 seteuid 的場(chǎng)景
seteuid 語(yǔ)義明確、副作用更少,是最合適的接口,實(shí)際上它們的歷史的演進(jìn)也是如此:setuid -> setreuid -> seteuid。下面的程序演示了基于 seteuid 做上圖中復(fù)雜的進(jìn)程特權(quán)控制的過(guò)程:
#include "../apue.h"
#include <sys/types.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
void print_ids (char const* prompt)
{
uid_t ruid = 0;
uid_t euid = 0;
uid_t suid = 0;
int ret = getresuid (&ruid, &euid, &suid);
if (ret == 0)
printf ("%s %d: ruid %d, euid %d, suid %d\n", prompt, getpid(), ruid, euid, suid);
else
err_sys ("getresuid");
}
int main (int argc, char *argv[])
{
uid_t ruid = getuid();
uid_t euid = geteuid();
if (ruid == euid)
{
printf ("ruid %d != euid %d, please set me set-uid before run this test !\n");
exit (1);
}
print_ids ("init");
int pid = fork ();
if (pid < 0)
err_sys ("fork");
else if (pid == 0)
{
// children
print_ids ("after fork");
int ret = seteuid (ruid);
if (ret == -1)
err_sys ("seteuid");
print_ids ("before exec");
execlp ("./setuid", "setuid", NULL);
err_sys ("execlp");
}
else
printf ("create child %u\n", pid);
wait(NULL);
print_ids("exit");
return 0;
}
做個(gè)簡(jiǎn)單說(shuō)明:
- 這個(gè)程序本身會(huì)被 set-uid,相當(dāng)于圖中的 101 進(jìn)程
- 它會(huì) fork 一個(gè)子進(jìn)程,并在其中 exec 程序 (./setuid),相當(dāng)于圖中的 102 進(jìn)程
- 將 seteuid 放置于 fork 之后 exec 之前,這樣做的好處是對(duì)父進(jìn)程沒(méi)有影響 (考慮父進(jìn)程多線程的場(chǎng)景)
- 被啟動(dòng)的 setuid 進(jìn)程不帶額外參數(shù),只會(huì)打印子進(jìn)程的 3 個(gè)ID 值,用于驗(yàn)證 SUID 值沒(méi)有從父進(jìn)程復(fù)制
下面是驅(qū)動(dòng)腳本:
#!/bin/sh
groupadd test
echo "create group ok"
useradd -g test foo
useradd -g test bar
foo_uid=$(id -u foo)
bar_uid=$(id -u bar)
echo "create user ok"
echo " foo: ${foo_uid}"
echo " bar: ${bar_uid}"
cd /tmp
chown bar:test ./fork_setuid
chmod u+s ./fork_setuid
su foo -c ./fork_setuid
userdel bar
userdel foo
echo "remove user ok"
rm -rf /home/bar
rm -rf /home/foo
echo "remove user home ok"
groupdel test
echo "delete group ok"
以 foo 用戶啟動(dòng)了一個(gè) set-uid 為 bar 的程序 (fork_setuid)。下面是腳本和程序的輸出:
> sudo sh fork_setuid.sh
create group ok
create user ok
foo: 1003
bar: 1004
init 29958: ruid 1003, euid 1004, suid 1004
create child 29959
after fork 29959: ruid 1003, euid 1004, suid 1004
before exec 29959: ruid 1003, euid 1003, suid 1004
29959: ruid 1003, euid 1003, suid 1003
exit 29958: ruid 1003, euid 1004, suid 1004
remove user ok
remove user home ok
delete group ok
做個(gè)簡(jiǎn)單說(shuō)明:
- 由于 set-uid,程序啟動(dòng)后 RUID = foo,EUID = SUID = bar
- fork 后父、子進(jìn)程以上值均沒(méi)有變化
- 子進(jìn)程 exec 前 seteuid 后,RUID = EUID = foo,SUID = bar
- 子進(jìn)程 exec 后,RUID = EUID = SUID = foo,徹徹底底失去了變身 bar 的機(jī)會(huì)
完全符合預(yù)期。做為對(duì)比,去掉程序中的 seteuid 調(diào)用,再次運(yùn)行:
> sudo sh fork_setuid.sh
create group ok
create user ok
foo: 1003
bar: 1004
init 14567: ruid 1003, euid 1004, suid 1004
create child 14568
after fork 14568: ruid 1003, euid 1004, suid 1004
14568: ruid 1003, euid 1004, suid 1004
exit 14567: ruid 1003, euid 1004, suid 1004
remove user ok
remove user home ok
delete group ok
這次子進(jìn)程 exec 后保留了 bar 身份。更進(jìn)一步,在原 demo 的基礎(chǔ)上為 ./setuid 設(shè)置 3 個(gè)參數(shù)來(lái)調(diào)用內(nèi)部的 seteuid,看它還能否恢復(fù) bar (1004) 的身份:
char tmp[128] = { 0 };
sprintf (tmp, "%u", euid);
execlp ("./setuid", "setuid", tmp, "noop", "noop", NULL);
新的程序輸出如下:
> sudo sh fork_setuid.sh
create group ok
create user ok
foo: 1003
bar: 1004
init 1646: ruid 1003, euid 1004, suid 1004
create child 1647
after fork 1647: ruid 1003, euid 1004, suid 1004
before exec 1647: ruid 1003, euid 1003, suid 1004
seteuid: Operation not permitted
exit 1646: ruid 1003, euid 1004, suid 1004
remove user ok
remove user home ok
delete group ok
的確不行。
最后需要補(bǔ)充一點(diǎn)的是,set-uid 標(biāo)志位對(duì)腳本文件不生效,原因其實(shí)已經(jīng)在“解釋器文件”一節(jié)中有過(guò)說(shuō)明:腳本文件只是解釋器的輸入,真正被啟動(dòng)的進(jìn)程是解釋器,只有將 set-uid 標(biāo)志加在解釋器上才能有效果,不過(guò)解釋器一般是一種通用的命令,具體要執(zhí)行的操作由腳本指定,如果將它指定為 set-uid root 的話,無(wú)疑會(huì)造成特權(quán)濫用。只有在封閉受控的系統(tǒng)中、沒(méi)有其它替代方法萬(wàn)不得已時(shí)才可出此下策。關(guān)于這方面更多的信息,可參考附錄 10。
進(jìn)程終止
關(guān)于進(jìn)程的終止,這篇《[apue] 進(jìn)程環(huán)境那些事兒》有過(guò)梳理,主要分 5 種正常終止與 3 種異常終止場(chǎng)景:
正常終止:
- 從 main 返回 (無(wú)論是否有返回值)
- 調(diào)用 exit
- 調(diào)用 _exit 或 _Exit
- 最后一個(gè)線程從其啟動(dòng)例程返回
- 最后一個(gè)線程調(diào)用 pthread_exit
異常終止:
- 調(diào)用 abort
- 接到一個(gè)信號(hào)并終止
- 最后一個(gè)線程對(duì)取消請(qǐng)求做出響應(yīng)
首先看正常終止場(chǎng)景下后兩個(gè)場(chǎng)景,它們都與線程相關(guān)。如果最后一個(gè)線程不是 main,那么當(dāng) main 返回或調(diào)用 exit 后進(jìn)程就終止了,不存在其它線程還能繼續(xù)跑的場(chǎng)景,所以 main 一定是進(jìn)程的最后一個(gè)線程,所謂它"從啟動(dòng)例程返回或調(diào)用 pthread_exit" 這句話沒(méi)有任何意義,因?yàn)?main 線程不是 pthread 庫(kù)創(chuàng)建的,也就是說(shuō)最后兩個(gè)場(chǎng)景在現(xiàn)實(shí)中并不存在,反正我是沒(méi)有試出來(lái)。
這樣進(jìn)程正常退出只要聚焦前三個(gè)場(chǎng)景就可以了,apue 上有一個(gè)圖非常經(jīng)典:
很好的描述了 exit 與 _exit、用戶進(jìn)程與內(nèi)核的關(guān)系。進(jìn)程異常終止雖然不走 exit,但在內(nèi)核有與正常終止相同的清理邏輯,估且稱之為 sys_exit,它的主要工作是關(guān)閉進(jìn)程所有打開(kāi)文件、釋放使用的存儲(chǔ)器 (memory) 等。
進(jìn)程終止?fàn)顟B(tài)
進(jìn)程退出后并不是什么信息也沒(méi)有留下,考慮一種場(chǎng)景,父進(jìn)程需要得知子進(jìn)程的退出碼 (exit(status)),系統(tǒng)為此保留了一部分進(jìn)程信息:
- 進(jìn)程 ID
- 終止?fàn)顟B(tài)
- 使用的 CPU 時(shí)間總量
- ……
這里的終止?fàn)顟B(tài)既包含了正常終止時(shí)的退出碼,也包含了異常終止時(shí)的信號(hào)等信息。
當(dāng)通過(guò) waitxxx 系統(tǒng)調(diào)用返回時(shí),終止?fàn)顟B(tài) (status) 一般作為整型返回,通過(guò)下面的宏可以提取退出碼、異常信號(hào)等信息:
- WIFEXITED (status):進(jìn)程正常終止為 true
- WEXITSTATUS (status):進(jìn)程正常終止的退出碼,為 exit 或 _exit 的低 8 位,關(guān)于 main 函數(shù) return 值與進(jìn)程 exit 參數(shù)的關(guān)系,參考《[apue] 進(jìn)程環(huán)境那些事兒》
- WIFSIGNALED (status):進(jìn)程異常終止為 true
- WTERMSIG (status):導(dǎo)致進(jìn)程異常終止的信號(hào)編號(hào)
- WCOREDUMP (status):產(chǎn)生 core 文件為 true (非 SUS 標(biāo)準(zhǔn))
- WIFSTOPPED (status):進(jìn)程被掛起為 true
- WSTOPSIG (status):導(dǎo)致進(jìn)程被掛起的信號(hào)編號(hào)
- WIFCONTINUED (status):進(jìn)程繼續(xù)運(yùn)行為 true
注意 wait 函數(shù)還可以獲取正在運(yùn)行的子進(jìn)程的狀態(tài),例如掛起或繼續(xù)運(yùn)行,但此時(shí)子進(jìn)程并未"終止",多用于 shell 這樣帶作業(yè)控制的終端程序。關(guān)于終止?fàn)顟B(tài)的更多信息將在討論 wait 函數(shù)族時(shí)進(jìn)一步介紹。
僵尸進(jìn)程與孤兒進(jìn)程
前文說(shuō)到,進(jìn)程退出后仍有一部分信息保留在系統(tǒng)中,這些信息雖然尺寸不大,但在未被 wait 之前會(huì)一直占據(jù)一個(gè)進(jìn)程表項(xiàng),而系統(tǒng)的進(jìn)程表項(xiàng)是有限的 (ulimit -u),如果這種僵尸進(jìn)程 (zombie) 太多,就會(huì)導(dǎo)致新進(jìn)程創(chuàng)建失敗。下面是基于 forkit 自制的一個(gè)例子:
$ ps -exjf
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
....
41707 41955 41707 41707 ? -1 S 1000 0:00 sshd: yunhai01@pts/1
41955 41957 41957 41957 pts/1 28035 Ss 1000 0:00 \_ bash -c /usr/bin/baas pass_bils_identity --baas_cred=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
41957 41982 41982 41957 pts/1 28035 S 1000 0:00 \_ /bin/bash -l XDG_SESSION_ID=39466 ANDROID_HOME=/ext/tools/android-sdk-linux TERM=xterm-256color SHELL=/bin/bash HISTSIZE=1000 SSH_CL
41982 28035 28035 41957 pts/1 28035 S+ 1000 0:00 \_ ./forkit XDG_SESSION_ID=39466 HOSTNAME=goodcitizen.bcc-gzhxy.baidu.com ANDROID_HOME=/ext/tools/android-sdk-linux SHELL=/bin/bash
28035 28036 28035 41957 pts/1 28035 Z+ 1000 0:00 \_ [forkit] <defunct>
....
其中 28035 是父進(jìn)程,28036 是子進(jìn)程,子進(jìn)程退出后父進(jìn)程沒(méi)有 wait 前,ps 查看它的狀態(tài)就是 Z (defunct)。僵尸進(jìn)程的產(chǎn)生是子進(jìn)程先于父進(jìn)程退出,如果父進(jìn)程先于子進(jìn)程退出呢?這就是孤兒進(jìn)程了 (orphan)。孤兒進(jìn)程將被過(guò)繼給 init 進(jìn)程 (進(jìn)程 ID = 1),無(wú)論父進(jìn)程是否為僵尸進(jìn)程,這是因?yàn)榻┦M(jìn)程無(wú)法 wait 回收任何子進(jìn)程。對(duì)上面的 forkit 稍加改造制作了 fork_zombie 程序:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
int pid = fork();
if (pid < 0)
{
// error
exit(1);
}
else if (pid == 0)
{
// child
printf ("%d spawn from %d\n", getpid(), getppid());
pid = fork();
if (pid < 0)
{
// error
exit(1);
}
else if (pid == 0)
{
// child
printf ("%d spawn from %d\n", getpid(), getppid());
sleep (10);
}
else
{
printf ("%d create %d\n", getpid(), pid);
}
}
else
{
printf ("%d create %d\n", getpid(), pid);
sleep (10);
}
printf ("after fork\n");
return 0;
}
在子進(jìn)程中繼續(xù) fork 子進(jìn)程,這個(gè)孫子進(jìn)程會(huì)存活比子進(jìn)程更長(zhǎng)的時(shí)間 (sleep 10),從而成為孤兒進(jìn)程:
$ ps -exjf
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
41707 41955 41707 41707 ? -1 S 1000 0:00 sshd: yunhai01@pts/1
41955 41957 41957 41957 pts/1 13744 Ss 1000 0:00 \_ bash -c /usr/bin/baas pass_bils_identity --baas_cred=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
41957 41982 41982 41957 pts/1 13744 S 1000 0:00 \_ /bin/bash -l XDG_SESSION_ID=39466 ANDROID_HOME=/ext/tools/android-sdk-linux TERM=xterm-256color SHELL=/bin/bash HISTSIZE=1000 SSH_CL
41982 13744 13744 41957 pts/1 13744 S+ 1000 0:00 \_ ./fork_zombie XDG_SESSION_ID=39466 HOSTNAME=goodcitizen.bcc-gzhxy.baidu.com ANDROID_HOME=/ext/tools/android-sdk-linux SHELL=/bin
13744 13745 13744 41957 pts/1 13744 Z+ 1000 0:00 \_ [fork_zombie] <defunct>
1 13746 13744 41957 pts/1 13744 S+ 1000 0:00 ./fork_zombie XDG_SESSION_ID=39466 HOSTNAME=goodcitizen.bcc-gzhxy.baidu.com ANDROID_HOME=/ext/tools/android-sdk-linux SHELL=/bin/bash TERM=x
其中 13744 是父進(jìn)程;13745 是子進(jìn)程,會(huì)成為僵尸進(jìn)程;13746 是孫子進(jìn)程,會(huì)成為孤兒進(jìn)程??梢钥吹焦聝哼M(jìn)程直接過(guò)繼給 init 進(jìn)程了。init 進(jìn)程會(huì)保證回收所有退出的子進(jìn)程,從而避免僵尸進(jìn)程數(shù)量太多的問(wèn)題。
上面無(wú)意間已經(jīng)揭示了一種避免僵尸進(jìn)程的方法:double fork,目前有共三種方法:
- wait 函數(shù)族
- SIGCHLD 信號(hào) (處理或忽略)
- double fork
前兩種方法在敝人的拙著《[apue] 等待子進(jìn)程的那些事兒》中都有記錄,歡迎大家翻閱~
需要注意的是第三種方法 double fork 只能避免孫子進(jìn)程不是僵尸進(jìn)程,子進(jìn)程還需要使用前兩種方法來(lái)避免成為僵尸進(jìn)程,這里用 wait 函數(shù)族多一些。
wait 函數(shù)族
在介紹 wait 函數(shù)族之前,先簡(jiǎn)單說(shuō)一下 SIGCHLD 信號(hào),當(dāng)子進(jìn)程退出時(shí),父進(jìn)程會(huì)接收到該信號(hào),面對(duì) SIGCHLD 父進(jìn)程有三種選擇:
- 默認(rèn):系統(tǒng)忽略
- 處理:增加信號(hào)處理函數(shù),在其中使用 wait 函數(shù)族回收子進(jìn)程 (可期望不被被阻塞)
- 忽略:SIG_IGN 顯示忽略信號(hào),子進(jìn)程會(huì)被自動(dòng)回收,對(duì) wait 函數(shù)族的影響后面介紹
有了這個(gè)鋪墊,再看 wait 接口定義:
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
wait
最基礎(chǔ)的獲取子進(jìn)程終止?fàn)顟B(tài)的接口,顧名思義,當(dāng)子進(jìn)程還在運(yùn)行時(shí),它還有同步等待子進(jìn)程退出的能力,以下是它在不同場(chǎng)景的表現(xiàn):
- 無(wú)任何子進(jìn)程:返回 -1,errno 設(shè)置為 ECHILD
- 子進(jìn)程全部在運(yùn)行:阻塞等待
- 有子進(jìn)程退出:獲取其中任意一個(gè)退出的子進(jìn)程狀態(tài)并返回其進(jìn)程 ID
為了避免阻塞,可期望在 SIGCHLD 信號(hào)處理器中調(diào)用 wait。如果 SIGCHLD 已通過(guò) SIG_IGN 顯示忽略,則一直阻塞直到所有子進(jìn)程退出,此時(shí)返回 -1,errno 設(shè)置為 ECHILD,同場(chǎng)景 1。
waitpid
相對(duì) wait 有三方面改進(jìn):
- 可指定等待的子進(jìn)程
- 可查詢子進(jìn)程狀態(tài)
- 可不阻塞試探
pid 參數(shù)比較靈活,既可指定進(jìn)程 ID 也可指定進(jìn)程組 ID:
- -1:任意子進(jìn)程,等價(jià)于 wait
- > 0:指定進(jìn)程 ID
- < -1:絕對(duì)值指定進(jìn)程組 ID
- 0:進(jìn)程組 ID 等于調(diào)用進(jìn)程的任意子進(jìn)程
注意第二個(gè)場(chǎng)景,雖然這里的 pid 可以任意指定,但如果不是調(diào)用進(jìn)程的子進(jìn)程,依然還是會(huì)出錯(cuò) (ECHILD)。
status 參數(shù)返回進(jìn)程終止?fàn)顟B(tài),成功返回時(shí),可用之前介紹過(guò)的 WIFXXX 進(jìn)行進(jìn)程狀態(tài)判斷;若不關(guān)心,可設(shè)置為 NULL。
options 參數(shù)可以指定以下值:
- WNOHANG:不阻塞,若無(wú)合適條件的子進(jìn)程則立即返回,返回值為 0
- WCONTINUED:用于作業(yè)控制,若已掛起的子進(jìn)程在繼續(xù)運(yùn)行后未報(bào)告狀態(tài),則返回其狀態(tài)
- WUNTRACED:用于作業(yè)控制,若已掛起的子進(jìn)程還未報(bào)告其狀態(tài),則返回其狀態(tài)
- WNOWAIT:不破壞子進(jìn)程終止?fàn)顟B(tài),后續(xù)仍可以被 wait (僅 Solaris 支持)
中間兩個(gè)選項(xiàng)用于作業(yè)控制,可用來(lái)獲取子進(jìn)程當(dāng)前的狀態(tài),成功返回時(shí),可用 WIFSTOPPED & WIFCONTINUED 去判斷進(jìn)程狀態(tài),一般為終端程序所用。
WNOHANG 在有合適條件的子進(jìn)程時(shí),會(huì)返回子進(jìn)程的 PID 與終止?fàn)顟B(tài),它提供了一種試探的能力。
waittid
是 waitpid 的一個(gè)變形,允許以更直觀的方式提供進(jìn)程 ID,它的 idtype 參數(shù)指定了 id 參數(shù)的含義:
- P_ALL:忽略 id,等待任一進(jìn)程
- P_PID:id 表示一個(gè)特定進(jìn)程,等待該進(jìn)程
- P_PGID:id 表示一個(gè)特定進(jìn)程組,等待該進(jìn)程組中任意一個(gè)進(jìn)程
從組合上講與 waitpid 功能一致,但實(shí)用性上不如 waitpid,waitpid 指定 0 就可以等待同進(jìn)程組的子進(jìn)程,waittid 還要明確指定一個(gè)進(jìn)程組,不方便。優(yōu)點(diǎn)是后者代碼看起來(lái)會(huì)更清晰一些。
options 參數(shù)與 waitpid 大同小異:
- WNOHANG:同上
- WNOWAIT:同上,所有平臺(tái)都支持
- WCONTINUED:同上
- WSTOPPED: 同 WUNTRACED
- WEXITED:等待正常退出的進(jìn)程
信號(hào)相關(guān)的信息存放在 infop 參數(shù)中,相比之前只能拿到一個(gè)信號(hào)編號(hào)要豐富多了。
wait3 & wait4
并非標(biāo)準(zhǔn)的一部分,但大部分 Unix 平臺(tái)均提供了,主要增加了被等待進(jìn)程 (及其所有子進(jìn)程) 使用的資源匯總 (rusage),除去這部分,wait3 等價(jià)于:
waitpid(-1, status, options);
wait4 等價(jià)于:
waitpid(pid, status, options);
資源匯總主要統(tǒng)計(jì)以下內(nèi)容:
struct rusage {
struct timeval ru_utime; /* user CPU time used */
struct timeval ru_stime; /* system CPU time used */
long ru_maxrss; /* maximum resident set size */
long ru_ixrss; /* integral shared memory size */
long ru_idrss; /* integral unshared data size */
long ru_isrss; /* integral unshared stack size */
long ru_minflt; /* page reclaims (soft page faults) */
long ru_majflt; /* page faults (hard page faults) */
long ru_nswap; /* swaps */
long ru_inblock; /* block input operations */
long ru_oublock; /* block output operations */
long ru_msgsnd; /* IPC messages sent */
long ru_msgrcv; /* IPC messages received */
long ru_nsignals; /* signals received */
long ru_nvcsw; /* voluntary context switches */
long ru_nivcsw; /* involuntary context switches */
};
就是一些 CPU 時(shí)間、內(nèi)存占用、頁(yè)錯(cuò)誤、信號(hào)接收次數(shù)等信息,有興趣的可以 man getrusage 查看。
進(jìn)程時(shí)間
司空見(jiàn)慣的 time 命令,是基于 wait3 實(shí)現(xiàn)時(shí)間統(tǒng)計(jì)的:
$ time sleep 3
real 0m3.001s
user 0m0.000s
sys 0m0.001s
其中
- real 為經(jīng)歷時(shí)間 (elapse)
- user 為用戶 CPU 時(shí)間
- sys 為系統(tǒng) CPU 時(shí)間
與 rusage 結(jié)構(gòu)中字段的對(duì)應(yīng)關(guān)系為:
- user:ru_utime
- sys: ru_stime
具體可參考附錄 7。
除了通過(guò) wait3 / wait4 獲取被等待進(jìn)程 CPU 時(shí)間外,通過(guò) times 接口還能獲取任意時(shí)間段的進(jìn)程 CPU 時(shí)間:
#include <sys/times.h>
struct tms {
clock_t tms_utime; /* user time */
clock_t tms_stime; /* system time */
clock_t tms_cutime; /* user time of children */
clock_t tms_cstime; /* system time of children */
};
clock_t times(struct tms *buf);
time 命令中 real、user、sys 三種時(shí)間與 times 的對(duì)應(yīng)關(guān)系如下:
- real:兩次 times 返回值的差值
- user:tms_utime 差值 + tms_cutime 差值
- sys:tms_stime 差值 + tms_cstime 差值
不過(guò)上述參數(shù)均以 clock_t 為單位,是時(shí)鐘滴答數(shù) (tick),還需將它轉(zhuǎn)換為時(shí)間:sec = tick / sysconf(_SC_CLK_TK)
,后者表示每秒的 tick 數(shù):
> getconf CLK_TCK
100
times 與 wait3 / wait4 另外一個(gè)差別是可以區(qū)分父、子進(jìn)程的時(shí)間消耗,不過(guò)要統(tǒng)計(jì)子進(jìn)程的 CPU 消耗,需要滿足以下兩個(gè)條件:
- 子進(jìn)程在 times 統(tǒng)計(jì)的時(shí)間段內(nèi)終止
- 終止時(shí)被父進(jìn)程 wait 到了
apue 中有一個(gè)絕好的例子,演示了 times 的使用方法,這里稍作修改驗(yàn)證上面的說(shuō)法:
#include "../apue.h"
#include <sys/types.h>
#include <sys/times.h>
#include <sys/wait.h>
static void pr_times (clock_t, struct tms *, struct tms *);
static void do_cmd (char *);
int main (int argc, char *argv[])
{
int i;
int status;
pid_t pid;
struct tms start, stop;
clock_t begin, end;
for (i=1; i<argc; i++)
do_cmd (argv[i]);
if ((begin = times (&start)) == -1)
err_sys ("times error");
while (1)
{
pid = wait (&status);
if (pid < 0)
{
printf ("wait all children\n");
break;
}
printf ("wait child %d\n", pid);
pr_exit (status);
if ((end = times (&stop)) == -1)
err_sys ("times error");
pr_times (end-begin, &start, &stop);
printf ("---------------------------------\n");
}
exit (0);
}
static void do_cmd (char *cmd)
{
int status;
pid_t pid = fork ();
if (pid < 0)
err_sys ("fork error");
else if (pid == 0)
{
// children
fprintf (stderr, "\ncommand: %s\n", cmd);
if ((status = system (cmd)) < 0)
err_sys ("system () error");
pr_exit (status);
exit (status);
}
else
printf ("fork child %d\n", pid);
}
static void pr_times (clock_t real, struct tms *start, struct tms *stop)
{
static long clktck = 0;
if (clktck == 0)
if ((clktck = sysconf (_SC_CLK_TCK)) < 0)
err_sys ("sysconf error");
clock_t diff = 0;
fprintf (stderr, " real: %7.2f\n", real / (double)clktck);
diff = (stop->tms_utime - start->tms_utime);
fprintf (stderr, " user: %7.2f (%ld)\n", diff/(double) clktck, diff);
diff = (stop->tms_stime - start->tms_stime);
fprintf (stderr, " sys: %7.2f (%ld)\n", diff/(double)clktck, diff);
diff = (stop->tms_cutime - start->tms_cutime);
fprintf (stderr, " child user: %7.2f (%ld)\n", diff/(double)clktck, diff);
diff = (stop->tms_cstime - start->tms_cstime);
fprintf (stderr, " child sys: %7.2f (%ld)\n", diff/(double)clktck, diff);
}
主要做了如下改動(dòng):
- 將傳入的命令放在子進(jìn)程中執(zhí)行 (do_cmd)
- 時(shí)間統(tǒng)計(jì)放在 while 循環(huán)中不斷 wait 子進(jìn)程后,直接沒(méi)有子進(jìn)程運(yùn)行后才退出
為了使例子更具真實(shí)性,傳遞進(jìn)來(lái)的兩個(gè)命令都執(zhí)行 dd 文件拷貝操作,只不過(guò)第二個(gè)命令拷貝的數(shù)據(jù)量是第一個(gè)的 2 倍:
> ./childtime "dd if=/dev/urandom of=./out1 bs=1M count=512" "dd if=/dev/urandom of=./out2 bs=1M count=1024"
fork child 3837
command: dd if=/dev/urandom of=./out1 bs=1M count=512
fork child 3838
command: dd if=/dev/urandom of=./out2 bs=1M count=1024
512+0 records in
512+0 records out
536870912 bytes (537 MB) copied, 7.2026 s, 74.5 MB/s
normal termination, exit status = 0
wait child 3837
normal termination, exit status = 0
real: 7.21
user: 0.00 (0)
sys: 0.00 (0)
child user: 0.00 (0)
child sys: 7.03 (703)
---------------------------------
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB) copied, 10.5758 s, 102 MB/s
normal termination, exit status = 0
wait child 3838
normal termination, exit status = 0
real: 10.58
user: 0.00 (0)
sys: 0.00 (0)
child user: 0.00 (0)
child sys: 17.45 (1745)
---------------------------------
wait all children
有以下發(fā)現(xiàn):
- dd1 結(jié)束時(shí),real 時(shí)間與 dd1 自己統(tǒng)計(jì)的接近 (7.21 => 7.2026)
- dd1 的 CPU 主要消耗在 sys 上,且與 real 時(shí)間接近 (7.03 => 7.21)
- dd2 結(jié)束時(shí),real 時(shí)間與 dd2 自己統(tǒng)計(jì)的接近 (10.58 => 10.5758)
- dd2 的 CPU 也消耗在 sys 上,但遠(yuǎn)遠(yuǎn)大于自己的 real 時(shí)間 (10.58 => 17.45),這是因?yàn)樗氖莾蓚€(gè)子進(jìn)程的 sys 時(shí)間,需要減去 dd1 的 sys 時(shí)間 (17.45 - 7.03 = 10.45),此時(shí)與自己的 real 時(shí)間接近
- 對(duì)比兩次 dd 的 sys CPU 消耗,會(huì)發(fā)現(xiàn) dd2 小于 2 倍 dd1 的消耗 (7.03 => 10.45),這與 dd2 平均速度大于 dd1 的結(jié)果吻合 (74.5 => 102),速度快了,耗時(shí)相應(yīng)的就降了;而速度提升,很可能與 dd1 退出不再搶占系統(tǒng)資源有關(guān)
- 總 CPU 時(shí)間 17.45,而總耗時(shí)只有 10.58,并行執(zhí)行任務(wù)有效的降低了等待時(shí)間
對(duì)程序稍加修改,每次等待到子進(jìn)程后更新 start 與 begin:
while (1)
{
pid = wait (&status);
if (pid < 0)
{
printf ("wait all children\n");
break;
}
printf ("wait child %d\n", pid);
pr_exit (status);
if ((end = times (&stop)) == -1)
err_sys ("times error");
pr_times (end-begin, &start, &stop);
begin = end;
start = stop;
printf ("---------------------------------\n");
}
再次運(yùn)行上面的程序:
> ./childtime "dd if=/dev/urandom of=./out1 bs=1M count=512" "dd if=/dev/urandom of=./out2 bs=1M count=1024"
fork child 7928
command: dd if=/dev/urandom of=./out2 bs=1M count=1024
512+0 records in
512+0 records out
536870912 bytes (537 MB) copied, 9.92325 s, 54.1 MB/s
normal termination, exit status = 0
wait child 7927
normal termination, exit status = 0
real: 9.93
user: 0.00 (0)
sys: 0.00 (0)
child user: 0.00 (0)
child sys: 6.93 (693)
---------------------------------
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB) copied, 13.499 s, 79.5 MB/s
normal termination, exit status = 0
wait child 7928
normal termination, exit status = 0
real: 3.57
user: 0.00 (0)
sys: 0.00 (0)
child user: 0.00 (0)
child sys: 10.34 (1034)
---------------------------------
wait all children
與之前相比,子進(jìn)程的 CPU 時(shí)間能真實(shí)的反映自己的情況了,其它的差別不大。通過(guò)這兩個(gè)例子,驗(yàn)證了 times 統(tǒng)計(jì)子進(jìn)程時(shí)間的第一個(gè)特點(diǎn):子進(jìn)程不結(jié)束時(shí),times 無(wú)法獲取它的進(jìn)程時(shí)間數(shù)據(jù);關(guān)于第二個(gè)特點(diǎn),感興趣的讀者,可以嘗試使用 sleep 代替 wait 子進(jìn)程,看看是否還能得到正確的統(tǒng)計(jì)。
關(guān)于子進(jìn)程時(shí)間統(tǒng)計(jì)的實(shí)現(xiàn),下面是我的一些個(gè)人想法:當(dāng)有子進(jìn)程結(jié)束且被父進(jìn)程 wait 后,它的進(jìn)程時(shí)間數(shù)據(jù)就會(huì)被累加到父進(jìn)程的子進(jìn)程時(shí)間中 (tms_cutime & tms_cstime),從而供 times 獲?。蝗粲脩羰褂玫氖?wait3 / wait4,則這份子進(jìn)程時(shí)間數(shù)據(jù)會(huì)和父進(jìn)程時(shí)間數(shù)據(jù)相加后再返回給用戶;若子進(jìn)程也 wait 了它的子進(jìn)程,那么這個(gè)數(shù)據(jù)還有"遞歸"效果,只不過(guò)在被累計(jì)至父進(jìn)程時(shí),孫子進(jìn)程的時(shí)間就和子進(jìn)程區(qū)分不開(kāi)了,統(tǒng)統(tǒng)計(jì)入子進(jìn)程一欄。
當(dāng)然了,這個(gè)機(jī)制是"君子協(xié)定",如果哪個(gè)子進(jìn)程沒(méi)有遵守規(guī)定,不給自己的子進(jìn)程"善后",那么最終統(tǒng)計(jì)出的時(shí)間就不準(zhǔn)確,不過(guò)總的來(lái)說(shuō)實(shí)際時(shí)間是只大不小,可以換個(gè)名稱如"至少進(jìn)程時(shí)間",就更貼切了。
system
glibc 基于 fork+exec+wait 玩了很多花樣,典型的如 system、popen & pclose:
#include <stdlib.h>
int system(const char *command);
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
system 可以接收任何 shell 能接受的命令,典型的如 date > log.txt
這種帶 shell 重定向功能的命令,甚至是一段 shell 腳本:for file in "*.c"; do echo $file; done
,當(dāng)參數(shù)為 NULL 時(shí),system 返回值表示 shell 是否可用,目前大多數(shù)系統(tǒng)中,都返回? 1。真實(shí) system 源碼比較復(fù)雜,書上模擬了一個(gè) system 的實(shí)現(xiàn):
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <errno.h>
int my_system (const char *cmdstring)
{
pid_t pid;
int status;
if (cmdstring == NULL)
return 1;
if ((pid = fork ()) < 0)
{
status = -1;
}
else if (pid == 0)
{
printf ("before calling shell: %s\n", cmdstring);
execl ("/bin/sh", "sh", "-c", cmdstring, (char *)0);
_exit (127);
}
else
{
while (waitpid (pid, &status, 0) < 0)
{
if (errno != EINTR)
{
printf ("wait cmd failed, errno = %d\n", errno);
status = -1;
break;
}
}
printf ("wait cmd, status = %d\n", status);
}
return (status);
}
通過(guò) fork 子進(jìn)程后 execl 調(diào)用 sh 進(jìn)程,并將 cmdstring 通過(guò) shell 的 -c 選項(xiàng)傳入,最后在父進(jìn)程 waitpid 等待子進(jìn)程終結(jié),中間考慮了信號(hào)中斷的場(chǎng)景。下面這個(gè)程序演示了 my_system 的調(diào)用:
#include "../apue.h"
extern int my_system (const char *cmdstring);
int main (int argc, char *argv[])
{
int status;
if (argc < 2)
err_quit ("command-line argument required");
if ((status = my_system (argv[1])) < 0)
err_sys ("system () error");
pr_exit (status);
exit (0);
}
通過(guò)命令行傳遞任何想要測(cè)試的內(nèi)容:
> ./tsys "date > log.txt"
normal termination, exit status = 0
> cat log.txt
Fri Mar 15 16:44:35 CST 2024
這樣做也有缺點(diǎn):所有傳遞給 system 接口的參數(shù)必需和命令放在一個(gè)參數(shù)中,假如想要執(zhí)行如下命令:ps -exjf
,不能寫成:./tsys ps -exjf
,必需寫成:./tsys "ps -exjf"
。不放在一起也可以,需要將調(diào)用 execl 改為 execv,并根據(jù) main 的 argv 參數(shù)構(gòu)造新的參數(shù)數(shù)組 (假設(shè)為 argv_new) 傳遞給新命令,而 argv_new 前兩個(gè)參數(shù)是固定的,分別是 sh 和 -c,最后以 NULL 結(jié)尾。
此外也可以不依賴 shell 直接基于 exec 函數(shù)族去做,將 cmdstring 中的命令名與參數(shù)傳遞給 exec 即可,仍以 ps 命令為例,最終執(zhí)行的是下面的代碼:
execlp (argv[1], argv[1] /*ps*/, argv[2] /*-exjf*/, (char *)0);
這里需要有兩個(gè)改變:
- 使用 execlp 或 execvp 以便在用戶只給命令名時(shí),可在 PATH 環(huán)境變量中查找命令所在位置
- 用戶將需要執(zhí)行的命令和參數(shù)單獨(dú)羅列而不是寫成一行,否則還需要解析 cmdstring 中的各個(gè)字段
結(jié)合以上分析,這里最合適的接口還是 execvp,直接傳遞 &argv[1]
就可以了。
不過(guò)這種直接調(diào)用 exec 的方式也有缺點(diǎn),就是不能享受 shell 提供的能力了,譬如:重定向、shell 腳本甚至 shell 元字符,所以目前大部分 glibc 的實(shí)現(xiàn)還是通過(guò)調(diào)用 shell 的。
關(guān)于 system 接口還有一個(gè)需要說(shuō)明的用例是 set-uid 程序,這主要表現(xiàn)在兩方面:
- 普通用戶使過(guò) system 調(diào)用 set-uid 為 root 的程序,新命令將具有特權(quán)
- 通過(guò) set-uid 為 root 獲取特權(quán)的進(jìn)程使用 system 調(diào)用普通程序,新命令將不具有特權(quán)
第一點(diǎn)比較好理解,主要是 exec 的工作;第二點(diǎn)比較依賴 shell,bash 2.0 之后的版本會(huì)檢查 RUID 與 EUID,如果不一致,會(huì)將 EUID 設(shè)置為 RUID,從而避免安全漏洞。如果直接使用 exec 啟動(dòng)新命令的話,特權(quán)是會(huì)被保留的。下面基于 setuid 程序和 tsys 做一個(gè)測(cè)試:
> ls -lh tsys setuid
-rwxr-xr-x 1 yunhai01 DOORGOD 53K Mar 15 15:54 setuid
-rwxr-xr-x 1 yunhai01 DOORGOD 52K Mar 15 15:54 tsys
> ./tsys ./setuid
29059: ruid 383278, euid 383278, suid 383278
normal termination, exit status = 0
> su
Password:
$ ./tsys ./setuid
29358: ruid 0, euid 0, suid 0
normal termination, exit status = 0
$ chown root:root tsys
$ chmod u+s tsys
$ suspend
[4]+ Stopped su
> ls -lh tsys setuid
-rwxr-xr-x 1 yunhai01 DOORGOD 53K Mar 15 15:54 setuid
-rwsr-xr-x 1 root root 52K Mar 15 15:54 tsys
> ./tsys ./setuid
29754: ruid 383278, euid 383278, suid 383278
normal termination, exit status = 0
> fg
su
$ chown yunhai01:DOORGOD tsys
$ exit
exit
> ls -lh tsys setuid
-rwxr-xr-x 1 yunhai01 DOORGOD 53K Mar 15 15:54 setuid
-rwxr-xr-x 1 yunhai01 DOORGOD 52K Mar 15 15:54 tsys
使用的 bash 版本 4.2.46。第三次 tsys 設(shè)置為 set-uid root 后,它啟動(dòng)的 ./setuid 的輸出來(lái)看,與預(yù)期一致,bash 攔截了不安全的特權(quán)繼承。
進(jìn)程核算
ps 命令可以查看系統(tǒng)當(dāng)前運(yùn)行的進(jìn)程,類似地,lastcomm 可以查看已經(jīng)終止的進(jìn)程,不過(guò)需要手動(dòng)開(kāi)啟進(jìn)程核算 (accounting,也稱為進(jìn)程會(huì)計(jì))。
accton
不同平臺(tái)都是通過(guò) accton 命令開(kāi)啟進(jìn)程核算的,但用法略有不同:
平臺(tái) | 默認(rèn)開(kāi)啟 | 帶文件開(kāi)啟 | 關(guān)閉 | 默認(rèn) acct 文件 |
Linux | sudo accton on | sudo accton /path/to/acctfiles | sudo accton off | /var/account/pacct |
macOS/FreeBSD | n/a | sudo accton /path/to/acctfiles | sudo accton | /var/account/acct |
Solaris | n/a | sudo accton /path/to/acctfiles | sudo accton | /var/adm/pacct |
做個(gè)簡(jiǎn)單說(shuō)明:
- Linux 上如果不指定 acct 文件路徑,則必需指定 on 或 off 參數(shù),不指定路徑時(shí)使用默認(rèn) acct 文件
- macOS 上不能指定 on 或 off 參數(shù),要開(kāi)啟進(jìn)程核算必需指定 acct 文件路徑,指定的路徑不存在時(shí)會(huì)出錯(cuò),需要手動(dòng)創(chuàng)建。不帶路徑時(shí)表示關(guān)閉核算
acct & struct acct
它們底層都是調(diào)用 acct 接口完成核算的開(kāi)啟和關(guān)閉:
#include <unistd.h>
int acct(const char *filename);
開(kāi)啟進(jìn)程核算后,內(nèi)核會(huì)在進(jìn)程終止時(shí)記錄一條信息到 acct 文件中,它在 C 語(yǔ)言中就是一個(gè)結(jié)構(gòu)體 struct acct。由于進(jìn)程核算并不是 POSIX 標(biāo)準(zhǔn)的一部分,各個(gè)平臺(tái)在這里的實(shí)現(xiàn)差異比較大,在 Linux 上:
#define ACCT_COMM 16
typedef u_int16_t comp_t;
struct acct {
char ac_flag; /* Accounting flags */
u_int16_t ac_uid; /* Accounting user ID */
u_int16_t ac_gid; /* Accounting group ID */
u_int16_t ac_tty; /* Controlling terminal */
u_int32_t ac_btime; /* Process creation time
(seconds since the Epoch) */
comp_t ac_utime; /* User CPU time */
comp_t ac_stime; /* System CPU time */
comp_t ac_etime; /* Elapsed time */
comp_t ac_mem; /* Average memory usage (kB) */
comp_t ac_io; /* Characters transferred (unused) */
comp_t ac_rw; /* Blocks read or written (unused) */
comp_t ac_minflt; /* Minor page faults */
comp_t ac_majflt; /* Major page faults */
comp_t ac_swaps; /* Number of swaps (unused) */
u_int32_t ac_exitcode; /* Process termination status
(see wait(2)) */
char ac_comm[ACCT_COMM+1];
/* Command name (basename of last
executed command; null-terminated) */
char ac_pad[X]; /* padding bytes */
};
enum { /* Bits that may be set in ac_flag field */
AFORK = 0x01, /* Has executed fork, but no exec */
ASU = 0x02, /* Used superuser privileges */
ACORE = 0x08, /* Dumped core */
AXSIG = 0x10 /* Killed by a signal */
};
在 macOS 上:
/*
* Accounting structures; these use a comp_t type which is a 3 bits base 8
* exponent, 13 bit fraction ``floating point'' number. Units are 1/AHZ
* seconds.
*/
typedef u_short comp_t;
struct acct {
char ac_comm[10]; /* name of command */
comp_t ac_utime; /* user time */
comp_t ac_stime; /* system time */
comp_t ac_etime; /* elapsed time */
time_t ac_btime; /* starting time */
uid_t ac_uid; /* user id */
gid_t ac_gid; /* group id */
short ac_mem; /* memory usage average */
comp_t ac_io; /* count of IO blocks */
dev_t ac_tty; /* controlling tty */
#define AFORK 0x01 /* forked but not execed */
#define ASU 0x02 /* used super-user permissions */
#define ACOMPAT 0x04 /* used compatibility mode */
#define ACORE 0x08 /* dumped core */
#define AXSIG 0x10 /* killed by a signal */
char ac_flag; /* accounting flags */
};
在 Solaris 上:
typedef ushort comp_t; /* pseudo "floating point" representation */
/* 3 bit base-8 exponent in the high */
/* order bits, and a 13-bit fraction */
/* in the low order bits. */
struct acct
{
char ac_flag; /* Accounting flag */
char ac_stat; /* Exit status */
uid_t ac_uid; /* Accounting user ID */
gid_t ac_gid; /* Accounting group ID */
dev_t ac_tty; /* control tty */
time_t ac_btime; /* Beginning time */
comp_t ac_utime; /* accounting user time in clock */
/* ticks */
comp_t ac_stime; /* accounting system time in clock */
/* ticks */
comp_t ac_etime; /* accounting total elapsed time in clock */
/* ticks */
comp_t ac_mem; /* memory usage in clicks (pages) */
comp_t ac_io; /* chars transferred by read/write */
comp_t ac_rw; /* number of block reads/writes */
char ac_comm[8]; /* command name */
};
/*
* Accounting Flags
*/
#define AFORK 01 /* has executed fork, but no exec */
#define ASU 02 /* used super-user privileges */
#define ACCTF 0300 /* record type */
#define AEXPND 040 /* Expanded Record Type - default */
下面這個(gè)表總結(jié)了各個(gè)平臺(tái)上 struct acct 的差異:
acct 字段 | Linux | macOS | FreeBSD | Solaris | |
ac_flag | AFORK (僅 fork 不 exec) | * | * | * | * |
ASU? (超級(jí)用戶進(jìn)程) | * | * | ? | * | |
ACOMPAT | ? | * | ? | ? | |
ACORE (發(fā)生 coredump) | * | * | * | ? | |
AXSIG (被信號(hào)殺死) | * | * | * | ? | |
AEXPND | ? | ? | ? | * | |
ac_stat (signal & core flag) | ? | ? | ? | * | |
ac_exitcode | * | ? | ? | ? | |
ac_uid / ac_gid / ac_tty | * | * | * | * | |
ac_btime / ac_utime / ac_stime / ac_etime | * | * | * | * | |
ac_mem | * (kB/?) | * (?) | * (?) | * (page/click) | |
ac_io | * (unused) | * (block) | * (block) | * (bytes) | |
ac_rw | * (unused) | ? | ? | * (block) | |
ac_comm | * (17) | * (10) | * (16) | * (8) |
做個(gè)簡(jiǎn)單說(shuō)明:
- 對(duì)于異常退出的場(chǎng)景,Linux & macOS & FreeBSD 是通過(guò) ac_flag 來(lái)記錄;Solaris 則通過(guò)單獨(dú)的 ac_stat 來(lái)記錄
- 對(duì)于正常退出的場(chǎng)景,只有 Linux 的 ac_exitcode 可以記錄進(jìn)程退出狀態(tài),其它平臺(tái)都沒(méi)有這個(gè)能力
- 對(duì)于進(jìn)程時(shí)間
- ac_btime 開(kāi)始時(shí)間為 epoch 時(shí)間,單位為秒
- ac_utime 為用戶 CPU 時(shí)間,單位為 ticks,含義同 rusage.ru_utime 或 tms.tms_utime
- ac_stime 為系統(tǒng) CPU 時(shí)間,單位為 ticks,含義同 rusage.ru_stime 或 tms.tms_stime
- ac_etime 為實(shí)際經(jīng)歷時(shí)間,單位為 ticks,含義同 time 命令中的 real 時(shí)間
- 對(duì)于進(jìn)程 IO 統(tǒng)計(jì)
- macOS & FreeBSD 傾向于使用塊作為單位,然而當(dāng)塊大小發(fā)生變更后,這個(gè)統(tǒng)計(jì)實(shí)際上會(huì)失真
- Solaris 做的好一些,使用的是字節(jié),它也有基于塊為單位的統(tǒng)計(jì),即 ac_rw
- Linux 則根本不具備統(tǒng)計(jì)進(jìn)程 IO 的能力,這兩個(gè)字段雖然存在卻總為 0
- 對(duì)于進(jìn)程名,各個(gè)平臺(tái)均支持,但長(zhǎng)度不一致。最長(zhǎng)為 Linux 17 字節(jié),最短為 Solaris 8 字節(jié),即使是最長(zhǎng)的 Linux,在目前看來(lái)也不夠用,不過(guò)考慮到記錄數(shù)太過(guò)龐大,這點(diǎn)可以原諒
進(jìn)程核算所需的各種數(shù)據(jù)都由內(nèi)核保存在進(jìn)程表中,并在一個(gè)新進(jìn)程被創(chuàng)建時(shí)置初值 (通常是 fork 之后的子進(jìn)程),每次進(jìn)程終止時(shí)都會(huì)追加一條結(jié)算記錄,這意味著 acct 文件中記錄的順序?qū)?yīng)于進(jìn)程終止順序而不是啟動(dòng)順序。配合上面的數(shù)據(jù)結(jié)構(gòu)會(huì)導(dǎo)致一個(gè)問(wèn)題——無(wú)法確定進(jìn)程的啟動(dòng)順序:
- 想推導(dǎo)進(jìn)程的啟動(dòng)順序時(shí),通常想到的方法是讀取全部結(jié)算記錄,按 ac_btime 進(jìn)行排序,但因日歷時(shí)間的精度是秒,在一秒內(nèi)可能啟動(dòng)了多個(gè)進(jìn)程,所以這種排序結(jié)果是不準(zhǔn)確的
- ac_etime 的單位是時(shí)鐘滴答 (ticks),每秒 ticks 通常在 60~128 之間,這個(gè)精度是夠了,但由于沒(méi)有記錄進(jìn)程的準(zhǔn)確結(jié)束時(shí)間,所以也無(wú)法反推它的準(zhǔn)確啟動(dòng)時(shí)間
第二種方案通過(guò)記錄進(jìn)程的準(zhǔn)確結(jié)束時(shí)間來(lái)反推進(jìn)程準(zhǔn)確啟動(dòng)時(shí)間,還不如就在一開(kāi)始記錄準(zhǔn)確啟動(dòng)時(shí)間。例如增加一個(gè)字段記錄啟動(dòng)毫秒數(shù),那 Unix 為何不這樣做?我能想到的一個(gè)答案——最小化存儲(chǔ)空間占用。增加一個(gè)毫秒字段至少需要 2 字節(jié),而一個(gè)短整型最大可以表達(dá) 65535,對(duì)于毫秒而言,數(shù)值 1000 以上的空間都無(wú)法使用,考慮到龐大記錄數(shù),這是一筆可觀的存儲(chǔ)浪費(fèi)。再看看 ac_utime / ac_stime / ac_etime 的設(shè)計(jì),通過(guò) 2 字節(jié)類型 (comp_t) 實(shí)現(xiàn)了 10ms 的時(shí)間精度,反過(guò)來(lái)看前者的設(shè)計(jì),在早期存儲(chǔ)空間寸土寸金的時(shí)代,簡(jiǎn)直就是一種無(wú)法容忍的揮霍行為。
由于進(jìn)程核算的起始錨定 fork 而非 exec、且只在進(jìn)程終止時(shí)記錄一條信息,這導(dǎo)致另外一個(gè)問(wèn)題——多次 exec 的進(jìn)程只記錄最后進(jìn)程的信息,前面 exec 過(guò)的進(jìn)程像“隱身”一樣消失了,就如同它從來(lái)沒(méi)在系統(tǒng)中存在過(guò)一樣,所以 acct 文件也不可全信,即某些進(jìn)程不在 acct 文件中不代表它不存在。最簡(jiǎn)單的做法,一個(gè)黑客可以在惡意程序的最后 exec 一個(gè)普通命令,來(lái)達(dá)到抹去 acct 記錄的目的。
exec loop
為了驗(yàn)證多次 exec 的場(chǎng)景,先寫一個(gè)可以循環(huán) exec 的測(cè)試程序:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main (int argc, char *argv[])
{
char const* exename=0;
if (argc > 1)
exename = argv[1];
else
exename = "date";
sleep (1);
printf ("%d, start exec: %s\n", getpid(), exename);
if (argc > 1)
execvp (exename, &argv[1]);
else
execlp (exename, exename, (char *)0);
printf ("should not reach here, exec failed?\n");
abort();
}
這個(gè)程序會(huì)將參數(shù)列表作為一個(gè)新程序執(zhí)行,其中 argv[1] 是新程序名,后面是它的參數(shù),如果參數(shù)不足 2,則使用默認(rèn)的 date 命令。如此就可以通過(guò)下面的調(diào)用執(zhí)行一個(gè)多次 exec 的進(jìn)程:
> ./exec_loop ./exec_loop ./exec_loop ./exec_loop ps -exjf
32166, start exec: ./exec_loop
32166, start exec: ./exec_loop
32166, start exec: ./exec_loop
32166, start exec: ps
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
11332 11364 11332 11332 ? -1 S 383278 0:00 sshd: yunhai01@pts/0
11364 11370 11370 11370 pts/0 32166 Ss 383278 0:00 \_ bash -c /usr/bin/baas login --baas_user=yunhai01 --baas_role=baas_all_pri
11370 11442 11442 11370 pts/0 32166 S 383278 0:00 \_ /bin/bash -l XDG_SESSION_ID=398726 TERM=xterm SHELL=/bin/bash SSH_CLI
11442 32166 32166 11370 pts/0 32166 R+ 383278 0:00 \_ ps -exjf XDG_SESSION_ID=398726 HOSTNAME=yunhai.bcc-bdbl.baidu.com
上面的過(guò)程開(kāi)啟 accton 錄制,通過(guò)書上的解讀 acct 文件的程序 (vacct) 解析后,得到下面的記錄:
> sudo ./vacct
clock tick: 100
sizeof (acct) = 64
...
ps e = 401.00, chars = 0, stat = 0:
...
系統(tǒng)中同時(shí)運(yùn)行的命令太多,通過(guò) grep 過(guò)濾,發(fā)現(xiàn)沒(méi)有任何 exec_loop 記錄;ps 記錄的 elapse 時(shí)間為 401 ticks,約 4 秒,和測(cè)試程序每 exec 一個(gè)命令 sleep? 1 秒吻合??雌饋?lái)確實(shí)只記錄了最后的一個(gè)進(jìn)程。
通過(guò)給 exec_loop 一個(gè)無(wú)法執(zhí)行的命令,看看會(huì)有何改變:
> ./exec_loop ./exec_loop ./exec_loop ./exec_loop this_cmd_not_exist
20313, start exec: ./exec_loop
20313, start exec: ./exec_loop
20313, start exec: ./exec_loop
20313, start exec: this_cmd_not_exist
should not reach here, exec failed?
Aborted (core dumped)
...
$ sudo ./vacct | grep exec_loop
exec_loop e = 403.00, chars = 0, stat = 134: D X
最終記錄的是上一個(gè)執(zhí)行成功的 exec_loop (elapse 4 秒),并且它的終止?fàn)顟B(tài)為接受信號(hào)后 (X) coredump (D),其中 stat 134 的高位 (第 8 位) 表示 coredump (128),低 7 位為 6 表示接受的信號(hào) (SIGABRT)。
對(duì)于 abort,Linux 的表現(xiàn)與書上 Solaris 例子不同,書上不會(huì)有 D & X 兩個(gè)標(biāo)志位。關(guān)于這一點(diǎn),通過(guò)重跑書上的那個(gè)多次 fork 的例子 (oacct) 也可以看清楚:
> ./vacct | grep -E 'oacct|dd|accton'
accton e = 0.00 , chars = 0, stat = 0: S
dd e = 0.00 , chars = 0, stat = 0:
oacct e = 200.00, chars = 0, stat = 0:
oacct e = 402.00, chars = 0, stat = 134: D X F
oacct e = 600.00, chars = 0, stat = 9: X F
oacct e = 800.00, chars = 0, stat = 0: F
accton e = 0.00 , chars = 0, stat = 0: S
accton e = 0.00 , chars = 0, stat = 0:
這個(gè)例子通過(guò) fork 生成了 4 個(gè)子進(jìn)程,除其中一個(gè)調(diào)用 exec 執(zhí)行 dd 外其它的均沒(méi)有 exec,下面對(duì)各個(gè) oacct 記錄做個(gè)說(shuō)明:
- 父進(jìn)程,sleep 2;exit 2
- 第一個(gè)子進(jìn)程,sleep 4;abort
- 第四個(gè)子進(jìn)程,sleep 6;kill
- 第三個(gè)子進(jìn)程,sleep 8;exit 0
- dd 為第二個(gè)子進(jìn)程,未 sleep 所以結(jié)束最快
各項(xiàng) ac_flag 值都能正確展示,其中 F 是首次出現(xiàn),表示只 fork 未 exec 的進(jìn)程,后面會(huì)看到,作者使用的 ac_flag 助詞符與系統(tǒng)命令完全一致。
lastcomm
如果不需要查看 ac_etime,使用 lastcomm 命令也不錯(cuò),以上面三個(gè)場(chǎng)景為例,分別輸出如下:
> sudo lastcomm yunhai01 -f /var/accout/pacct.old
ps yunhai01 pts/0 0.01 secs Mon Mar 25 11:55
> sudo lastcomm yunhai01
exec_loop DX yunhai01 pts/0 0.00 secs Mon Mar 25 12:10
$ sudo lastcomm yunhai01 -f /var/account/pacct-20240322
oacct F yunhai01 pts/0 0.00 secs Thu Mar 21 15:02
oacct F X yunhai01 pts/0 0.00 secs Thu Mar 21 15:02
oacct F DX yunhai01 pts/0 0.00 secs Thu Mar 21 15:02
oacct yunhai01 pts/0 0.00 secs Thu Mar 21 15:02
dd yunhai01 pts/0 0.00 secs Thu Mar 21 15:02
因?yàn)樵L問(wèn)的 acct 文件具有超級(jí)用戶權(quán)限,所以這里也需要 sudo 提權(quán)。關(guān)于 lastcomm 輸出的各個(gè)字段,man 手冊(cè)頁(yè)有一段說(shuō)明:
For each entry the following information is printed:
+ command name of the process
+ flags, as recorded by the system accounting routines:
S -- command executed by super-user
F -- command executed after a fork but without a following exec
C -- command run in PDP-11 compatibility mode (VAX only)
D -- command terminated with the generation of a core file
X -- command was terminated with the signal SIGTERM
+ the name of the user who ran the process
+ time the process started
分別是:命令名、標(biāo)志位、用戶、終端、進(jìn)程啟動(dòng)時(shí)間等,標(biāo)志位與 ac_flag 有如下對(duì)應(yīng)關(guān)系:
- S:ASU
- F:AFORK
- C:ACOMPAT
- D:ACORE
- X:AXSIG
另外除了啟動(dòng)時(shí)間,還打印了 xxx secs 的信息,經(jīng)過(guò)對(duì)比應(yīng)該不是 ac_etime,懷疑是 ac_utime、ac_stime 之一或之和。
通過(guò) -f 可指定不同于默認(rèn)路徑的 acct 文件;通過(guò)參數(shù)可以篩選命令、用戶、終端,關(guān)鍵字的順序不重要,lastcomm 將用它們?cè)谌蜻M(jìn)行匹配,只要有任一匹配成功,就會(huì)輸出記錄,相當(dāng)于是 OR 的關(guān)系。例如用戶想篩選名為 foo 的用戶,會(huì)將名為 foo 的命令也給篩選出來(lái),為了減少這種烏龍事件,lastcomm 也支持指定域匹配:
> lastcomm --strict-match --command ps --user yunhai01 --tty pts/0
這種情況下,各個(gè)條件之間是 AND 的關(guān)系,只有匹配所有條件的記錄才會(huì)輸出。
dump-acct
如果僅在 Linux 上使用,dump-acct 可以輸出比 lastcomm 更豐富的信息:
> dump-acct /var/account/pacct-20240322 | grep 383278
dd |v3| 0.00| 0.00| 0.00|383278|100000|108096.00| 0.00| 25047 25046|Thu Mar 21 15:02:35 2024
oacct |v3| 0.00| 0.00| 200.00|383278|100000| 4236.00| 0.00| 25045 19543|Thu Mar 21 15:02:35 2024
oacct |v3| 0.00| 0.00| 402.00|383278|100000| 4236.00| 0.00| 25046 1|Thu Mar 21 15:02:35 2024
oacct |v3| 0.00| 0.00| 600.00|383278|100000| 4236.00| 0.00| 25049 25048|Thu Mar 21 15:02:35 2024
oacct |v3| 0.00| 0.00| 800.00|383278|100000| 4236.00| 0.00| 25048 1|Thu Mar 21 15:02:35 2024
記錄比較多,通過(guò) grep 過(guò)濾了用戶 ID。各列含義如下:
- 命令名:ac_comm
- 版本號(hào),關(guān)于 v3 請(qǐng)參考 Linux 擴(kuò)展:acct_v3
- 用戶 CPU 時(shí)間:ac_utime
- 系統(tǒng) CPU 時(shí)間:ac_stime
- 經(jīng)歷時(shí)間:ac_etime
- 用戶 ID:v3 擴(kuò)展
- 組 ID:v3 擴(kuò)展
- 內(nèi)存使用:ac_mem
- IO:ac_io
- 進(jìn)程 ID:v3 擴(kuò)展
- 父進(jìn)程 ID:v3 擴(kuò)展
- 啟動(dòng)時(shí)間:ac_btime
信息比 lastcomm 豐富,但是缺少對(duì) ac_flag 的展示,這方面還得借助書上的實(shí)用工具 (vacct)。
比較有趣的是上例中父進(jìn)程 ID 的打印,從進(jìn)程號(hào)可以直觀的發(fā)現(xiàn)進(jìn)程啟動(dòng)順序:
- 25045:oacct 為父進(jìn)程,sleep 2;exit 2
- 25046:oacct 為第一個(gè)子進(jìn)程,sleep 4;abort。父進(jìn)程為 1 應(yīng)當(dāng)是進(jìn)程終止時(shí)父進(jìn)程 25045 已結(jié)束被過(guò)繼給 init 進(jìn)程
- 25047:dd 為第二個(gè)子進(jìn)程,未 sleep 所以結(jié)束最快。父進(jìn)程為 25046 是第一個(gè)子進(jìn)程,正常
- 25048:oacct 為第三個(gè)子進(jìn)程,sleep 8;exit 0。父進(jìn)程為 1 應(yīng)當(dāng)是進(jìn)程終止時(shí)父進(jìn)程 25047 已結(jié)束被過(guò)繼給 init 進(jìn)程
- 25049:oacct 為第四個(gè)子進(jìn)程,sleep 6,kill 9。父進(jìn)程 2504 是第三個(gè)子進(jìn)程,正常
acct_v3 版本確實(shí)強(qiáng)大,通過(guò)父子進(jìn)程 ID 復(fù)習(xí)了進(jìn)程關(guān)系中的孤兒進(jìn)程概念。
對(duì)于上節(jié)中 lastcomm xxx secs 打印的是 ac_utime 還是 ac_stime,這節(jié)可以給出答案:
sudo dump-acct /var/account/pacct.old | grep 383278
ps |v3| 0.00| 1.00| 401.00|383278|100000|153344.00| 0.00| 858 11442|Mon Mar 25 11:55:33 2024
對(duì)比之前 lastcomm 的輸出,ps 命令的 secs 字段為 0.1,對(duì)應(yīng)的是 1 ticks,也就是這里的 ac_stime,可見(jiàn)肯定不是 ac_utime,但到底是系統(tǒng) CPU 時(shí)間還是兩者之和,由于這條記錄的 ac_utime 為 0,沒(méi)有辦法確認(rèn)了,需要再找一條記錄看看:
> sudo lastcomm -f /var/account/pacct.old | grep argus_process_s
argus_process_s root __ 0.13 secs Mon Mar 25 11:55
> sudo dump-acct /var/account/pacct.old | grep argus_process_s
argus_process_s |v3| 12.00| 1.00| 36.00| 0| 0| 41160.00| 0.00| 908 20078|Mon Mar 25 11:55:36 2024
?這條記錄看得更清楚了,0.13 s = 13 ticks = ac_utime (12) + ac_stime (1),所以最終的結(jié)論是:lastcomm 中 xxx secs 輸出的是進(jìn)程 CPU 總耗時(shí) (ac_utime + ac_stime)。
sa
lastcomm & dump-acct 是針對(duì)單個(gè)記錄的,如果想統(tǒng)計(jì)多個(gè)記錄的信息,例如 CPU 總耗時(shí)、磁盤 IO 總次數(shù)、命令啟動(dòng)次數(shù)等,就需要使用 sa 命令了,針對(duì) lastcomm 中的 oacct 的例子使用 sa 統(tǒng)計(jì),得到如下輸出:
> sa /var/account/pacct-20240322
314 2.77re 0.01cp 0avio 19357k
3 0.02re 0.01cp 0avio 10295k argus_process_s
13 0.04re 0.00cp 0avio 32597k ***other*
3 0.01re 0.00cp 0avio 188160k top
2 0.00re 0.00cp 0avio 234880k sudo
51 2.40re 0.00cp 0avio 1091k sleep
46 0.00re 0.00cp 0avio 3309k killall
28 0.00re 0.00cp 0avio 2454k monitor_webdir_*
21 0.00re 0.00cp 0avio 1093k wc
17 0.00re 0.00cp 0avio 2482k awk
17 0.00re 0.00cp 0avio 1099k date
14 0.00re 0.00cp 0avio 4888k ls
14 0.00re 0.00cp 0avio 2558k monitor_baas_ag*
14 0.00re 0.00cp 0avio 1627k hostname
13 0.00re 0.00cp 0avio 1096k cat
9 0.00re 0.00cp 0avio 2274k grep
8 0.00re 0.00cp 0avio 422528k noah-client*
6 0.00re 0.00cp 0avio 3310k bc
5 0.00re 0.00cp 0avio 2392k sh
4 0.00re 0.00cp 0avio 1091k getconf
3 0.30re 0.00cp 0avio 1059k oacct*
3 0.00re 0.00cp 0avio 181760k bscpserver
3 0.00re 0.00cp 0avio 1097k tr
3 0.00re 0.00cp 0avio 28848k gpu.sh
3 0.00re 0.00cp 0avio 2552k supervise.bscps*
3 0.00re 0.00cp 0avio 703k accton
3 0.00re 0.00cp 0avio 94k migstat
3 0.00re 0.00cp 0avio 91k gpustat
2 0.00re 0.00cp 0avio 1100k tail
各列含義如下:命令出現(xiàn)次數(shù)、總經(jīng)歷時(shí)間、總 CPU 時(shí)間、平均 IO 次數(shù)、CPU 利用率、命令名。
與一般命令不同的是,sa 并不輸出一個(gè)標(biāo)題行,而是將各列的含義追加在數(shù)值后面,man 中對(duì)此有詳細(xì)說(shuō)明:
The output fields are labeled as follows:
cpu
sum of system and user time in cpu minutes
re
"elapsed time" in minutes
k
cpu-time averaged core usage, in 1k units
avio
average number of I/O operations per execution
tio
total number of I/O operations
k*sec
cpu storage integral (kilo-core seconds)
u
user cpu time in cpu seconds
s
system time in cpu seconds
各個(gè)列與 struct acct 的對(duì)應(yīng)關(guān)系是:
- u:ac_utime
- s:ac_stime
- cpu:ac_stime + ac_utime
- re:ac_etime,單位為 min
- avio/tio:ac_io
- k/k*sec:?
注意上面的輸出中沒(méi)有 dd 命令,原來(lái)為了簡(jiǎn)潔,小于 2 次的命令都會(huì)被折疊到 other 一欄中,需要使用 -a 選項(xiàng)展示它們:
> sa /var/account/pacct-20240322 -a
314 2.77re 0.01cp 0avio 19357k
3 0.02re 0.01cp 0avio 10295k argus_process_s
1 0.00re 0.00cp 0avio 57104k abrt-action-sav
3 0.01re 0.00cp 0avio 188160k top
2 0.00re 0.00cp 0avio 234880k sudo
1 0.00re 0.00cp 0avio 57088k abrt-server
1 0.00re 0.00cp 0avio 31120k abrt-hook-ccpp
1 0.00re 0.00cp 0avio 56576k abrt-handle-eve
51 2.40re 0.00cp 0avio 1091k sleep
46 0.00re 0.00cp 0avio 3309k killall
28 0.00re 0.00cp 0avio 2454k monitor_webdir_*
21 0.00re 0.00cp 0avio 1093k wc
17 0.00re 0.00cp 0avio 2482k awk
17 0.00re 0.00cp 0avio 1099k date
14 0.00re 0.00cp 0avio 4888k ls
14 0.00re 0.00cp 0avio 2558k monitor_baas_ag*
14 0.00re 0.00cp 0avio 1627k hostname
13 0.00re 0.00cp 0avio 1096k cat
9 0.00re 0.00cp 0avio 2274k grep
8 0.00re 0.00cp 0avio 422528k noah-client*
6 0.00re 0.00cp 0avio 3310k bc
5 0.00re 0.00cp 0avio 2392k sh
4 0.00re 0.00cp 0avio 1091k getconf
3 0.30re 0.00cp 0avio 1059k oacct*
3 0.00re 0.00cp 0avio 181760k bscpserver
3 0.00re 0.00cp 0avio 1097k tr
3 0.00re 0.00cp 0avio 28848k gpu.sh
3 0.00re 0.00cp 0avio 2552k supervise.bscps*
3 0.00re 0.00cp 0avio 703k accton
3 0.00re 0.00cp 0avio 94k migstat
3 0.00re 0.00cp 0avio 91k gpustat
2 0.00re 0.00cp 0avio 1100k tail
1 0.03re 0.00cp 0avio 1059k oacct
1 0.00re 0.00cp 0avio 164096k bcm-agent*
1 0.00re 0.00cp 0avio 27024k dd
1 0.00re 0.00cp 0avio 18752k ssh
1 0.00re 0.00cp 0avio 4066k iptables
1 0.00re 0.00cp 0avio 3280k lsmod
1 0.00re 0.00cp 0avio 2394k sh*
1 0.00re 0.00cp 0avio 1115k dmidecode
1 0.00re 0.00cp 0avio 90k gpu-int
這次在倒數(shù)第 7 行有 dd 了。默認(rèn)是按 cpu 倒序排列的,sa 也提供了大量選項(xiàng)來(lái)按其它字段排序,感興趣的可自行 man 查看。
命令名后面的星號(hào)表示 AFORK 標(biāo)志,例如上面的輸出中,有兩條 oacct 記錄,1 個(gè)不帶星號(hào)的是父進(jìn)程,3 個(gè)帶星號(hào)的是 fork 未 exec 的子進(jìn)程,另外 1 個(gè)子進(jìn)程就是 dd。
指定 -m 選項(xiàng)可以按用戶級(jí)別查看統(tǒng)計(jì)信息:
> sa /var/account/pacct-20240322 -m
314 2.77re 0.01cp 0avio 19357k
root 309 2.44re 0.01cp 0avio 19569k
yunhai01 5 0.33re 0.00cp 0avio 6252k
除了第一列增加了用戶名,最后一列刪除了命令名外,其余各列與之前相同。
總結(jié)一下,dump-acct、lastcomm、sa 命令之于 acct 文件的關(guān)系,與 w、who、last、lastb、ac 命令之于 utmp、wtmp、btmp 文件的關(guān)系類似,關(guān)于后者,可以參考《[apue] Unix 系統(tǒng)數(shù)據(jù)文件那些事兒》。
參考
[1].?Linux/Unix分配進(jìn)程ID的方法以及源代碼實(shí)現(xiàn)
[2].?Linux下如何在進(jìn)程中獲取虛擬地址對(duì)應(yīng)的物理地址
[3].?fork() 和 Solaris 線程的特殊問(wèn)題
[4].?Linux Clone函數(shù)
[5].?淺談linux下進(jìn)程最大數(shù)、最大線程數(shù)、進(jìn)程打開(kāi)的文件數(shù)
[6].?在 Linux 上以樹(shù)狀查看文件和進(jìn)程
[7].?time命令busybox源碼
[8].?對(duì)argv可能的誤解
[9].?解釋器、解釋器文件
[10]. 如何使腳本的set-user-id位起作用
[11].?Linux setuid使用文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-844321.html
[12].?acct——accounting utilities 統(tǒng)計(jì)工具文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-844321.html
到了這里,關(guān)于[apue] 進(jìn)程控制那些事兒的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!