UNIX環境高級編程——進程控制

1、進程標識符

     ID爲0的進程是調度進程,經常被稱爲交換進程。該進程是內核的一部分,它並不執行任何磁盤上的程序,所以也被稱爲系統進程。進程ID 1一般是init進程,在自舉過程結束時由內核調用。init一般讀與系統有關的初始化文件,並將系統引導到一個狀態(例如多用戶)。init進程決不會終止。它是一個普通的用戶進程,可是它以超級用戶特權運行。ubuntu

pid_t getpid(void);          返回值:調用進程的進程ID
pid_t getppid(void);        返回值:調用進程的父進程ID
uid_t getuid(void);           返回值:調用進程的實際用戶ID
uid_t geteuid(void);         返回值:調用進程的有效用戶ID
gid_t getgid(void);           返回值:調用進程的實際組ID
gid_t getegid(void);         返回值:調用進程的有效組ID

2、 fork系統調用

包含頭文件 <sys/types.h> 和 <unistd.h>
函數功能:建立一個子進程
函數原型
         pid_t  fork(void);
參數:無參數。
返回值:
若是成功建立一個子進程,對於父進程來講返回子進程ID
若是成功建立一個子進程,對於子進程來講返回值爲0
若是爲-1表示建立失敗

小程序


(1)使用fork函數獲得的子進程從父進程的繼承了整個進程的地址空間,包括:進程上下文、進程堆棧、內存信息、打開的文件描述符、信號控制設置、進程優先級、進程組號、當前工做目錄、根目錄、資源限制、控制終端等。
數組



(2)子進程與父進程的區別在於:
一、父進程設置的鎖,子進程不繼承;
二、各自的進程ID和父進程ID不一樣;
三、子進程的未決告警被清除;
四、子進程的未決信號集設置爲空集。
bash


(3)fork系統調用須要注意的地方:數據結構

     fork系統調用以後,父子進程將交替執行。
     若是父進程先退出,子進程還沒退出那麼子進程的父進程將變爲init進程。(注:任何一個進程都必須有父進程,子進程是孤兒進程
     若是子進程先退出,父進程還沒退出,那麼子進程必須等到父進程捕獲到了子進程的退出狀態才真正結束,不然這個時候子進程就成爲僵進程。子進程退出會發送SIGCHLD信號給父進程,能夠選擇忽略或使用信號處理函數接收處理就能夠避免殭屍進程。異步

     殭屍進程:一個子進程在其父進程尚未調用wait()或waitpid()的狀況下退出。這個子進程就是殭屍進程。
     孤兒進程:一個父進程退出,而它的一個或多個子進程還在運行,那麼那些子進程將成爲孤兒進程。孤兒進程將被init進程(進程號爲1)所收養,並由init進程對它們完成狀態收集工做。
函數


(4)寫時複製 copy on write測試

     若是多個進程要讀取它們本身的那部分資源的副本,那麼複製是沒必要要的。
     每一個進程只要保存一個指向這個資源的指針就能夠了。
     若是一個進程要修改本身的那份資源的「副本」,那麼就會複製那份資源。這就是寫時複製的含義ui

     例如fork就是基於寫時複製,只讀代碼段是能夠共享的。this

     若使用vfork()則在還沒調用exec以前,父子進程是共享同一個地址空間,不像fork()同樣會進行拷貝 


(5)fork以後父子進程共享文件


子進程繼承了父進程打開的文件描述符,故每一個打開文件的引用計數爲2


(6)fork與vfork

     在fork還沒實現copy on write以前。Unix設計者很關心fork以後未馬上執行exec所形成的地址空間浪費,因此引入了vfork系統調用。
     vfork有個限制,子進程必須馬上執行_exit或者exec函數。
     即便fork實現了copy on write,效率也沒有vfork高,可是咱們不推薦使用vfork,由於幾乎每個vfork的實現,都或多或少存在必定的問題。


(7)fork和vfork的區別

vfork()用法與fork()類似.可是也有區別,具體區別歸結爲如下3點:

1.  fork():子進程拷貝父進程的數據段,代碼段。vfork():子進程與父進程共享數據段

2.  fork():父子進程的執行次序不肯定

     vfork():保證子進程先運行在調用exec或exit(注意:return也不行)以前與父進程數據是共享的,在它調用exec或exit以後父進程纔可能被調度運行。

3.  vfork()保證子進程先運行,在它調用exec或exit以後父進程纔可能被調度運行。若是在調用這兩個函數以前子進程依賴於父進程的進一步動做,則會致使死鎖。

4.  當須要改變共享數據段中變量的值,則拷貝父進程。

下面經過幾個例子加以說明:

第一:子進程拷貝父進程的代碼段的例子:

#include<sys/types.h>  
#include<unistd.h>  
#include<stdio.h>  
  
int main()  
{  
    pid_t pid;  
    pid = fork();  
    if(pid<0)  
        printf("error in fork!\n");  
    else if(pid == 0)  
        printf("I am the child process,ID is %d\n",getpid());  
    else   
        printf("I am the parent process,ID is %d\n",getpid());  
    return 0;  
  
} 
運行結果: 

I am the child process,ID is 4711  
I am the parent process,ID is 4710 


再來看一個拷貝數據段的例子: 

#include<sys/types.h>  
#include<unistd.h>  
#include<stdio.h>  
  
int main()  
{  
    pid_t pid;  
    int cnt = 0;  
    pid = fork();  
    if(pid<0)  
        printf("error in fork!\n");  
    else if(pid == 0)  
    {  
        cnt++;  
        printf("cnt=%d\n",cnt);  
        printf("I am the child process,ID is %d\n",getpid());  
    }  
    else  
    {  
        cnt++;  
        printf("cnt=%d\n",cnt);  
        printf("I am the parent process,ID is %d\n",getpid());  
    }  
    return 0;  
} 
運行結果: 

[root@localhost fork]# ./fork2  
cnt=1  
I am the child process,ID is 5077  
cnt=1  
I am the parent process,ID is 5076 

那麼再來看看vfork ()吧。若是將上面程序中的fork ()改爲vfork(),運行結果是什麼 
樣子的呢? 

[root@localhost fork]# gcc -o fork3 fork3.c   
[root@localhost fork]# ./fork3 
cnt=1
I am the child process,ID is 4520
cnt=2
I am the parent process,ID is 4519
cnt=1
I am the child process,ID is 4521
cnt=2
I am the parent process,ID is 4519
cnt=1
I am the child process,ID is 4522
cnt=2
後面無限循環

這樣上面程序中的fork ()改爲vfork()後,vfork ()建立子進程並無調用exec 或exit,注意:就算是最後又執行return 0也是不行的。因此最終將致使死鎖。 
怎麼改呢?看下面程序: 

#include<sys/types.h>  
#include<unistd.h>  
#include<stdio.h>  
  
int main()  
{  
    pid_t pid;  
    int cnt = 0;  
    pid = vfork();  
    if(pid<0)  
        printf("error in fork!\n");  
    else if(pid == 0)  
    {  
        cnt++;  
        printf("cnt=%d\n",cnt);  
        printf("I am the child process,ID is %d\n",getpid());  
       _exit(0);  
    }  
    else  
    {  
        cnt++;  
        printf("cnt=%d\n",cnt);  
        printf("I am the parent process,ID is %d\n",getpid());  
    }  
    return 0;  
  
} 
     若是沒有_exit(0)的話,子進程沒有調用exec 或exit,因此父進程是不可能執行的,在子 進程調用exec 或exit 以後父進程纔可

能被調度運行。 因此咱們加上_exit(0);使得子進程退出,父進程執行,這樣else 後的語句就會被父進程執行, 又因在子進程調用

exec 或exit以前與父進程數據是共享的,因此子進程退出後把父進程的數 據段count改爲1 了,子進程退出後,父進程又執行,最終就將count變成了2,看下實際 運行結果: 

[root@localhost fork]# gcc -o fork3 fork3.c   
[root@localhost fork]# ./fork3  
cnt=1  
I am the child process,ID is 4711  
cnt=2  
I am the parent process,ID is 4710 


示例程序:

/* 若是父進程先退出,子進程還沒退出那麼子進程的父進程將變爲init進程。(注:任何一個進程都必須有父進程)
 * 若是子進程先退出,父進程還沒退出,那麼子進程必須等到父進程捕獲到了子進程的退出狀態才真正結束,
 * 不然這個時候子進程就成爲僵進程。
 */
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<signal.h>

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

int main(int argc, char *argv[])
{
    signal(SIGCHLD, SIG_IGN); // 避免產生殭屍進程,忽略SIGCHLD信號
    printf("before fork pid=%d\n", getpid());
    int fd;
    fd = open("test.txt", O_WRONLY);
    if (fd == -1)
        ERR_EXIT("open error");

    pid_t pid;
    pid = fork(); // 寫時複製copy on write,只讀代碼段能夠共享
    /* 若使用vfork()則在還沒調用exec以前,父子進程是共享同一個地址空間,
     * 不像fork()同樣會進行拷貝 */
    if (pid == -1)
        ERR_EXIT("fork error");

    if (pid > 0)
    {
        printf("this is parent\n");
        printf("parent pid=%d child pid=%d\n", getpid(), pid);
        write(fd, "parent", 6); // 父子進程共享一個文件表
        sleep(10);
    }

    else if (pid == 0)
    {
        printf("this is child\n");
        printf("child pid=%d parent pid=%d\n", getpid(), getppid());
        write(fd, "child", 5);
    }

    return 0;
}
測試輸出以下:

huangcheng@ubuntu:~$ ./a.out
before fork pid=5400
this is parent
parent pid=5400 child pid=5401
this is child
child pid=5401 parent pid=5400
huangcheng@ubuntu:~$ cat test.txt
parentchild

能夠看到由於共享一個文件表,故文件偏移也共享,父子進程打印進test.txt文件的內容是緊隨的而不是從頭開始的。

測試輸出以下:

huangcheng@ubuntu:~$ ./a.out > temp.out
huangcheng@ubuntu:~$ cat temp.out
before fork pid=5492
this is child
child pid=5493 parent pid=5492
before fork pid=5492                  //第二次輸出
this is parent
parent pid=5492 child pid=5493

     標準I/O庫是帶緩衝的。若是標準輸出連到終端設備,則它是行緩衝的,不然它是全緩衝的。當以交互方式運行該程序,只獲得printf輸出一次,其緣由是標準輸出緩衝區由換行符沖洗。可是當將標準輸出重定向到一個文件時,卻獲得printf輸出兩次。其緣由是,在fork以前調用了printf一次,但當調用fork時,該行數據仍在緩衝區中,而後在將父進程數據空間複製到子進程中時,該緩衝區也被複制到子進程。因而那時父、子進程各自有了帶該行內容的標準I/O緩衝區


3、exit函數

     進程的最後一個線程在其啓動例程中執行返回語句。可是,該線程的返回值不會用做進程的返回值。當最後一個線程從其啓動例程返回時,該進程以終止狀態0返回。

     進程的最後一個線程調用pthread_exit函數,這種狀況下,進程的終止狀態老是0,這與傳送給pthread_exit的參數無關。

在異常終止狀況下,內核(不是進程自己)產生一個指示其異常終止緣由的終止狀態。在任意一種狀況下,該終止進程的父進程都能用wait或者waitpid函數取得其終止狀態。

 

4、殭屍進程

     當子進程退出的時候,內核會向父進程發送SIGCHLD信號,子進程的退出是個異步事件(子進程能夠在父進程運行的任什麼時候刻終止)。
     一個已經終止,可是其父進程還沒有對其進行善後處理(獲取終止子進程的有關信息,釋放它仍佔用的資源)的進程爲殭屍進程它只保留最小的一些內核數據結構,以便父進程查詢子進程的退出狀態。
     父進程查詢子進程的退出狀態能夠用wait/waitpid函數。

     利用命令ps,能夠看到有標記爲Z的進程就是殭屍進程。

 

5、如何避免殭屍進程

     當一個子進程結束運行時,它與其父進程之間的關聯還會保持到父進程也正常地結束運行或者父進程調用了wait/waitpid才了結止。
     進程表中表明子進程的數據項是不會馬上釋放的,雖然再也不活躍了,可子進程還停留在系統裏,由於它的退出碼還須要保存起來以備父進程中後續的wait/waitpid調用使用。它將稱爲一個「僵進程」。

     調用wait或者waitpid函數查詢子進程退出狀態,此方法父進程會被掛起(waitpid能夠設置不掛起)。
     若是不想讓父進程掛起,能夠在父進程中加入一條語句:signal(SIGCHLD,SIG_IGN);表示父進程忽略SIGCHLD信號,該信號是子進程退出的時候向父進程發送的。也能夠不忽略SIGCHLD信號,而接收在信號處理函數中調用wait/waitpid。

     殺死殭屍進程的辦法:殺死進程的父進程,殭屍進程稱爲孤兒進程,過繼給1號進程initinit始終會負責清理殭屍進程。

 

6、wait函數

     當一個進程正常或異常終止時,內核就向父進程發送SIGCHLD信號。

頭文件<sys/types.h>和<sys/wait.h>
     函數功能:當咱們用fork啓動一個進程時,子進程就有了本身的生命,並將獨立地運行。有時,咱們須要知道某個子進程是否已經結束了,咱們能夠經過wait安排父進程在子進程結束以後。
函數原型:pid_t wait(int *status)
函數參數:status:該參數能夠得到你等待子進程的信息
返回值:成功等待子進程函數返回等待子進程的ID

 

     wait系統調用會使父進程暫停執行,直到它的一個子進程結束爲止。
     返回的是子進程的PID,它一般是結束的子進程
     狀態信息容許父進程斷定子進程的退出狀態,即從子進程的main函數返回的值或子進程中exit語句的退出碼。
     若是status不是一個空指針,狀態信息將被寫入它指向的位置

 

經過如下的宏定義能夠得到子進程的退出狀態

WIFEXITED(status) 若是子進程正常結束,返回一個非零值
WEXITSTATUS(status) 若是WIFEXITED非零,返回子進程退出碼
WIFSIGNALED(status) 子進程由於捕獲信號而終止,返回非零值
WTERMSIG(status) 若是WIFSIGNALED非零,返回信號代碼
WIFSTOPPED(status) 若是子進程被暫停,返回一個非零值
WSTOPSIG(status) 若是WIFSTOPPED非零,返回一個信號代碼

 

7、waitpid函數

函數功能: 用來等待某個特定進程的結束

函數原型: pid_t waitpid(pid_t pid, int *status,int options)
 參數:
         status:若是不是空,會把狀態信息寫到它指向的位置
         options:容許改變waitpid的行爲,最有用的一個選項是WNOHANG,它的做用是防止waitpid把調用者的執行掛起等待
返回值:若是成功返回等待子進程的ID,失敗返回-1

 

對於waitpid的p i d參數的解釋與其值有關:
pid == -1      等待任一子進程。因而在這一功能方面waitpid與wait等效。
pid > 0          等待其進程ID與p i d相等的子進程。
pid == 0       等待其組ID等於調用進程的組I D的任一子進程。換句話說是與調用者進程同在一個組的進程。
pid < -1        等待其組ID等於p i d的絕對值的任一子進程。

 

8、wait和waitpid函數的區別

      兩個函數都用於等待進程的狀態變化包括正常退出,被信號異常終止,被信號暫停,被信號喚醒繼續執行等。

     在一個子進程終止前, wait 使其調用者阻塞,waitpid 有一選擇項,可以使調用者不阻塞。
     waitpid並不僅能等待第一個終止的子進程—它有若干個選擇項,能夠控制它所等待的特定進程。
     實際上wait函數是waitpid函數的一個特例。

 

示例程序:

#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/wait.h>

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

int main(int argc, char *argv[])
{
    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork error");

    if (pid == 0)
    {
        sleep(3);
        printf("this is child\n");
        //      exit(100);
        abort();
    }

    printf("this is parent\n");
    int status;
    int ret;
    ret = wait(&status); // 阻塞等待子進程退出
    //  ret = waitpid(-1, &status, 0);
    //  ret = waitpid(pid, &status, 0);
    /* waitpid能夠等待特定的進程,而不只僅是第一個退出的子進程
     * 且能夠設置option爲WNOHANG,即不阻塞等待 */
    printf("ret=%d, pid=%d\n", ret, pid);
    if (WIFEXITED(status))
        printf("child exited normal exit status=%d\n", WEXITSTATUS(status));
    else if (WIFSIGNALED(status))
        printf("child exited abnormal signal number=%d\n", WTERMSIG(status));
    else if (WIFSTOPPED(status))
        printf("child stopped signal number=%d\n", WSTOPSIG(status));

    return 0;
}
輸出爲:

huangcheng@ubuntu:~$ ./a.out
this is parent
this is child
ret=2195, pid=2195
child exited abnormal signal number=6
    說明子進程被信號異常終止,由於咱們調用了abort(), 即產生SIGABRT信號將子進程終止,能夠查到此信號序號爲6。若是咱們不使用abort 而是exit(100), 則應該輸出:

huangcheng@ubuntu:~$ ./a.out
this is parent
this is child
ret=2214, pid=2214
child exited normal exit status=100

9、 exec替換進程映象

     在進程的建立上Unix採用了一個獨特的方法,它將進程建立與加載一個新進程映象分離。這樣的好處是有更多的餘地對兩種操做進行管理。當咱們建立了一個進程以後,一般將子進程替換成新的進程映象,這能夠用exec系列的函數來進行。固然,exec系列的函數也能夠將當前進程替換掉。


10、exec關聯函數組

包含頭文件<unistd.h>
功能用exec函數能夠把當前進程替換爲一個新進程。exec名下是由多個關聯函數組成的一個完整系列,頭文件<unistd.h>
原型:

     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 execvpe(const char *file, char *const argv[],char *const envp[]);

參數
path參數表示你要啓動程序的名稱包括路徑名
arg參數表示啓動程序所帶的參數
返回值:成功返回0,失敗返回-1

execl,execlp,execle(都帶「l」)的參數個數是可變的,參數以一個空指針結束。
execv、execvp和execvpe的第二個參數是一個字符串數組,新程序在啓動時會把在argv數組中給定的參數傳遞到main

名字含字母「p」的函數會搜索PATH環境變量去查找新程序的可執行文件。若是可執行文件不在PATH定義的路徑上,就必須把包括子目錄在內的絕對文件名作爲一個參數傳遞給這些函數。

名字最後一個字母爲"e"的函數能夠自設環境變量。

這些函數一般都是用execve實現的,這是一種約定俗成的作法,並非非這樣不可。

int execve(const char *filename, char *const argv[], char *const envp[]);

注意,前面6個函數都是C庫函數,而execve是一個系統調用。



示例程序:

爲了演示自設環境變量的功能,先寫個小程序,能夠輸出系統的環境變量

#include<stdio.h>
#include<unistd.h>

extern char **environ;

int main(void)
{
    printf("hello pid=%d\n", getpid());
    int i;
    for (i = 0; environ[i] != NULL; i++)
        printf("%s\n", environ[i]);
    return 0;
}
其中environ是全局變量但沒有在頭文件中聲明,因此使用前須要外部聲明一下。輸出以下:

huangcheng@ubuntu:~$ ./a.out
hello pid=5597
TERM=vt100
SHELL=/bin/bash
XDG_SESSION_COOKIE=0ba97773224d90f8e6cd57345132dfd0-1368605430.130657-1433620678
SSH_CLIENT=192.168.232.1 8740 22
SSH_TTY=/dev/pts/0
USER=simba
......................

即輸出了一些系統環境的變量,變量較多,省略輸出。

咱們前面在講到fcntl 函數時未講到當cmd參數取F_SETFD時的情形,即設置文件描述符的標誌,現結合exec系列函數講解以下:

#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)
/* 這幾個庫函數都會調用execve這個系統調用 */
int main(int argc, char *argv[])
{
    char *const args[] = {"ls", "-l", NULL};
    printf("Entering main ... \n");
    //  execlp("ls", "ls", "-l", NULL); // 帶p會搜索PATH
    //  execl("/bin/ls", "ls", "-l", NULL); // 帶l爲可變參數
    //  execvp("ls", args); //args數組參數傳遞給main
    //  execv("/bin/ls", args);

    int ret;
    //  ret = fcntl(1, F_SETFD, FD_CLOEXEC);
    /* FD_CLOSEXEC被置位爲1(在打開文件時標誌爲O_CLOEXEC也會置位),
     * 即在執行execve時將標準輸出的文件描述符關閉,
     * 即下面替換的pid_env程序不會在屏幕上輸出信息
     */
    //  if (ret == -1)
    //      perror("fcntl error");

    char *const envp[] = {"AA=11", "BB=22", NULL};
    ret = execle("./pid_env", "pid_enV", NULL, envp); // 帶e能夠自帶環境變量
    //  execvpe("ls", args, envp);
    if (ret == -1)
        perror("exec error");
    printf("Exiting main ... \n");

    return 0;
}
     咱們使用了exec系列函數進行舉例進程映像的替換,最後未被註釋的execle函數須要替換的程序正是咱們前面寫的輸出系統環境變量的小程序,但由於execle能夠自設環境變量,故被替換後的進程輸出的環境變量不是系統的那些而是自設的,輸出以下:

huangcheng@ubuntu:~$ ./a.out
Entering main ... 
hello pid=5643
AA=11
BB=22

     若是咱們將上面 fcntl 函數的註釋打開了,即設置當執行exec操做時,關閉標準輸出(fd=1)的文件描述符,也就是說下面替換的pid_env程序不會在屏幕上輸出信息。

     由於若是替換進程映像成功,那麼直接到替換進程的main開始執行,不會返回,故不會輸出Exiting main ...

相關文章
相關標籤/搜索