一、什麼是進程
進程是正在執行的程序實例。執行程序時,內核會將程序代碼載入虛擬內存,為程序變量分配空間,在內核中建立相應的數據結構,以記錄與進程有關的各種信息(比如,進程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 = 52.由結果可以看出,子進程改變了全局變量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時,父進程創建子進程之後,下一次循環父進程還會繼續執行與剛才相同的操作,且產生的子進程與上一次產生的子進程屬於同一個父進程。上一次產生的子進程也會和父進程在下一次循環產生自己的子進程,直到循環結束。