歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> Linux技術

Linux中fork()函數

一、什麼是進程

進程是正在執行的程序實例。執行程序時,內核會將程序代碼載入虛擬內存,為程序變量分配空間,在內核中建立相應的數據結構,以記錄與進程有關的各種信息(比如,進程ID、用戶ID、組ID以及終止狀態等)。

簡單來說,就是”執行一個程序或命令“就可以出發一個事件而獲取一個進程ID。也就是說,程序被觸發後,執行者的權限與屬性、程序代碼與數據等會被加載到內存,操作系統並給予這個內存單元一個標識符(進程ID)。

進程可以使用系統調用fork()來創建一個子進程。子進程獲得父進程的數據空間、堆和棧的副本。父進程和子進程並不共享這些存儲空間部分,共享正文段,也就是在內存中被標記為只讀的程序文本段。例如在Linux shell中鍵入命令,ps時,shell會創建一個進程,這個子進程執行ps。

kernel@Ubuntu:~/Desktop$ ps -l
F S   UID    PID   PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000  17637  17627  3  80   0 -  6909 wait   pts/1    00:00:00 bash
0 R  1000  17650  17637  0  80   0 -  3554 -      pts/1    00:00:00 ps
二、系統調用fork()

1. fork()函數被調用一次,但有兩個返回值。父進程返回新創建的子進程ID,子進程返回0。此時的0不是進程ID為0的進程(ID為0的進程通常是調度進程,是內核的一部分,它並不執行任何磁盤上的進程,被稱為系統進程)。

實例:

#include <unistd.h>
#include <sys/types.h>
#include "err.h"

void err_sys(const char * fmt, ...) __attribute((noreturn));

int    globvar = 5;
char   buf[] = "global String\n";

int main(void) {

    int     var;
    pid_t   pid;

    var = 28;
    if (write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1)  /*將buf緩沖區的字符由標准輸出輸出到終端*/
        err_sys("write error");
    printf("before fork");

    if ((pid = fork()) < 0)    /*創建子進程*/
        err_sys("fork erro");
    if (!pid) {                /*執行子進程*/
        globvar++;
        var++;
    }
    else 
        sleep(2);              /*父進程等待子進程執行完成*/

    printf("pid = %d, ppid = %d, var = %d, globlvar = %d\n", getpid(), getppid(), var, globvar);

    exit(0);
}
結果如下:

kernel@Ubuntu:~/Desktop$ gcc -g fork.c -o fork
kernel@Ubuntu:~/Desktop$ ./fork 
global String
before fork
pid = 21398, ppid = 21397, var = 29, globlvar = 6
pid = 21397, ppid = 20518, var = 28, globlvar = 5
2.由結果可以看出,子進程改變了全局變量globvar和局部變量var的值,但對於父進程來說,卻是沒有改變的。所以說子進程是父進程創建的副本,它們並不共享存儲空間的部分。而且在調用fork()之前,有”beforefork” 輸出,而在創建子進程之後,子進程並沒有輸出該字符串。因為printf()是標准I/O函數,而且輸出流是連接到終端,通常使用行緩沖。遇到換行符標准I/O執行I/O操作,將緩沖區的內容寫到終端,並清理緩沖區。但是將標准輸出重定向到一個文件時,在執行fork函數時,該行數據還在緩沖區,然後父進程將數據復制到子進程,該緩沖區的數據也被復制到子進程。

kernel@Ubuntu:~/Desktop$ ./fork > file
kernel@Ubuntu:~/Desktop$ cat file
global String
before fork
pid = 21493, ppid = 21492, var = 29, globlvar = 6
before fork
pid = 21492, ppid = 20518, var = 28, globlvar = 5
因為write是不帶緩沖的,其數據只寫到標准輸出一次,所以子進程數據空間並沒有write寫入的數據。

3.在fork之後,父進程和子進程的執行順序是不確定的,這取決於內核的調度算法。因此在pid> 0,父進程執行是休眠2s,讓pid == 0時,子進程執行完畢後,再執行父進程。

還有一種情況,fork創建子進程,子進程通過調用exec的一系列函數,執行另一部分程序。

4.fork之後處理文件有兩種方式

(1)父進程等待子進程完成。在這種情況下,父進程無需對其描述符做任何處理。當子進程終止後,它曾讀、寫操作的任一共享描述符的文件偏移量已經做了相應的更新。

#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include "err.h"

void err_sys(const char * fmt, ...) __attribute__((noreturn));

int main(void) {

   pid_t     pid;
   off_t     currpos;
   char      buf1[] = "testing the lseek\n";
   char      buf2[] = "child add the lseek\n";
   int       fd;

   if ((fd = open("/home/kernel/Desktop/testfile", O_RDWR | O_CREAT | O_TRUNC, )) < 0)        /* 讀、寫創建或打開文件 */
       err_sys("open error");

   if (write(fd, buf1, sizeof(buf1) - 1) != sizeof(buf1) - 1) 
       err_sys("write error");

   currpos = lseek(fd, 0, SEEK_CUR);                /* 當前文件偏移量 */
   printf("before fork the lseek is %ld\n", currpos);

   if ((pid = fork()) < 0) 
       err_sys("fork error");
   if (!pid) {
       if (write(fd, buf2, sizeof(buf2) - 1) != sizeof(buf2) - 1)   
           err_sys("write error");    
       currpos = lseek(fd, 0, SEEK_CUR);           /*子進程文件偏移量 */
       printf("now child lseek is %ld\n", currpos);         
   }
   else {
       sleep(2);
       currpos = lseek(fd, 0, SEEK_CUR);          /* 父進程文件偏移量 */
       printf("now parent lseek is %ld\n", currpos);
   }

   exit(0);
}
結果如下:

kernel@Ubuntu:~/Desktop$ gcc -g fork1.c -o fork1
kernel@Ubuntu:~/Desktop$ ./fork1
before fork the lseek is 18
now child lseek is 38
now parent lseek is 38
(2)父進程和子進程各自執行不同的程序段。在這種情況下,在fork之後,父進程和子進程各自關閉它們不需使用的文件描述符,就這樣就不會干擾對方使用的描述符,這種方法是網絡服務進程經常使用的。

5.現在可以看看父進程和子進程再去創建各自的子進程是什麼情況。

#include <unistd.h>
#include <sys/types.h>
#include "err.h"

void err_sys(const char * fmt, ...) __attribute__((noreturn));

int main(void) {

    pid_t    pid;
    int      i;

    for (i = 0; i < 2; i++) {
        if ((pid = fork()) < 0)
            err_sys("fork error");
        if (!pid) 
            printf("i = %d\tChild\tpid = %d\tppid = %d\n", i, getpid(), getppid());
        else 
            printf("i = %d\tParent\tpid = %d\tppid = %d\n", i, getpid(), getppid());
    }

    exit(0);

}
結果如下:
i = 0	Parent	pid = 14972	ppid = 12028
i = 0	Child	pid = 14973	ppid = 14972
i = 1	Parent	pid = 14972	ppid = 12028
i = 1	Parent	pid = 14973	ppid = 14972
i = 1	Child	pid = 14974	ppid = 14972
i = 1	Child	pid = 14975	ppid = 14973

結論:當 i = 0,parent0創建第一個子進程child0。i = 1,parent0與child0會創建各自的子進程child1與child2,此時child0也是一個父進程parent1,所以parent0與parent1會輸出。i = 2時,parent0,child0/parent1,child1,child2結束循環並退出。

三、總結

在使用fork函數時,子進程和父進程的執行順序不定,有可能會出現競爭,有內核的調度算法實現。如果子進程比父進程先結束,並且父進程沒有對子進程的資源進行回收,所以會產生僵死進程,並且kill其父進程,僵死進程也可能會還會存在。所以父進程應使用wait或waipid函數獲取子進程相關信息,內核可以釋放子進程占用的存儲區,關閉打開的文件。如果父進程先於子進程結束,init進程會成為子進程的父進程,有init進程收養。

在循環中使用fork時,父進程創建子進程之後,下一次循環父進程還會繼續執行與剛才相同的操作,且產生的子進程與上一次產生的子進程屬於同一個父進程。上一次產生的子進程也會和父進程在下一次循環產生自己的子進程,直到循環結束。

Copyright © Linux教程網 All Rights Reserved