《Linux/UNIX系统编程手册》第24章 进程的创建
关键词:fork、vfork、exit、wait、execve等等。
1. fork()、exit()、wait()以及execve()的简介
- fork()允许以进程创建一新进程:子进程获得父进程的栈、数据段、堆和执行文本段的拷贝。
- exit()终止一进程,将进程占用的所有资源归还内核,交其进行再次分配。父进程可使用系统调用wait()来获取进程的退出状态。
- wait()目的有二:一,如果子进程尚未调用exit()终止,那么wait()会挂起父进程直至子进程终止。二,子进程的终止状态通过wait()的status参数返回。
- execve(pathname, argv, envp)加载一个新程序到当前进程的内存。这将丢弃现存的程序文本段,并未新程序重新创建栈、数据段以及堆。
下图对fork()、exit()、wait()以及exece()之间的相互协同做了总结:
#include "tlpi_hdr.h" static int idata = 111; /* Allocated in data segment */ int main(int argc, char *argv[]) { int istack = 222; /* Allocated in stack segment */ pid_t childPid; switch (childPid = fork()) { case -1: errExit("fork"); case 0: idata *= 3; istack *= 3; break; default: sleep(3); /* Give child a chance to execute */ break; } /* Both parent and child come here */ printf("PID=%ld %s idata=%d istack=%d\n", (long) getpid(), (childPid == 0) ? "(child) " : "(parent)", idata, istack); exit(EXIT_SUCCESS); }
执行结果:
PID=31319 (child) idata=333 istack=666 PID=31318 (parent) idata=111 istack=222
如果去掉sleep(3)存在如下几种情况,这说明父进程和子进程谁先被调度到是不确定的。
PID=2795 (child) idata=333 istack=666 PID=2794 (parent) idata=111 istack=222 PID=2796 (parent) idata=111 istack=222 PID=2797 (child) idata=333 istack=666
2. 创建新进程:fork()
#include <unistd.h> pid_t fork(void); In parent: returns process ID of child on success, or –1 on error; in successfully created child: always returns 0
完成fork()调用后,父子进程都会从fork()返回处继续执行。
父子进程执行相同的程序文本段,但却拥有不同的栈段、数据段以及堆段。子进程的栈、数据、堆开始时都是对父进程内存相应各部分的完全复制。fork()后,每个进程均可修改各自的栈、数据以及堆中的变量。
程序通过fork()返回值来区分父子进程:返回0表示在子进程中;其他值表示在父进程中,-1表示错误,>0表示子进程pid。
调用fork()之后,系统将率先执行父进程还是子进程,是无法确定的。
2.1 父子进程间的文件共享
fork()后,子进程会获得父进程所有文件描述符的副本,意味着父子进程中对应的描述符均指向相同的打开文件句柄,包含当前文件偏移量以及文件状态标志。一个打开文件的这些属性因之而在父子进程间实现了共享。
#include <sys/stat.h> #include <fcntl.h> #include <sys/wait.h> #include "tlpi_hdr.h" int main(int argc, char *argv[]) { int fd, flags; char template[] = "/tmp/testXXXXXX"; setbuf(stdout, NULL); /* Disable buffering of stdout */ /* Open a temporary file, set its file offset to some arbitrary value, and change the setting of one of the open file status flags. */ fd = mkstemp(template); if (fd == -1) errExit("mkstemp"); printf("File offset before fork(): %lld\n", (long long) lseek(fd, 0, SEEK_CUR)); flags = fcntl(fd, F_GETFL); if (flags == -1) errExit("fcntl - F_GETFL"); printf("O_APPEND flag before fork() is: %s\n", (flags & O_APPEND) ? "on" : "off"); switch (fork()) { case -1: errExit("fork"); case 0: /* Child: change file offset and status flags */ if (lseek(fd, 1000, SEEK_SET) == -1) errExit("lseek"); flags = fcntl(fd, F_GETFL); /* Fetch current flags */ if (flags == -1) errExit("fcntl - F_GETFL"); flags |= O_APPEND; /* Turn O_APPEND on */ if (fcntl(fd, F_SETFL, flags) == -1) errExit("fcntl - F_SETFL"); _exit(EXIT_SUCCESS); default: /* Parent: can see file changes made by child */ if (wait(NULL) == -1) errExit("wait"); /* Wait for child exit */ printf("Child has exited\n"); printf("File offset in parent: %lld\n", (long long) lseek(fd, 0, SEEK_CUR)); flags = fcntl(fd, F_GETFL); if (flags == -1) errExit("fcntl - F_GETFL"); printf("O_APPEND flag in parent is: %s\n", (flags & O_APPEND) ? "on" : "off"); exit(EXIT_SUCCESS); } }
子进程改变文件偏移量以及标志,退出。然后父进程随即获取文件偏移量和标志。结果如下:
File offset before fork(): 0 O_APPEND flag before fork() is: off Child has exited File offset in parent: 1000 O_APPEND flag in parent is: on
2.2 fork()的内存语义
内核对父子进程的数据段和堆栈作如下处理:
- 内核将每一进程的代码段标记为只读,从而使进程无法修改自身代码。父子进程可共享同一代码段,fork()为子进程创建代码段时,其所构建的一系列进程级页表项均指向与父进程相同的物理内存页帧。
- 对于父进程数据段、堆和栈中的各页,内核采用COW技术。fork()之后,内核会捕获所有父进程或紫金城对这些页面的修改企图,并为将要修改的页面创建拷贝。系统将新的页面拷贝分配给遭内核捕获的进程,还会对子进程的相应也表项做适当调整。
3. 系统调用vfork()
vfork()可以为调用进程创建一个新的子进程,然而,vfork()是为了进程立即执行exec()的程序而专门设计的。
#include <unistd.h> pid_t vfork(void); In parent: returns process ID of child on success, or –1 on error;in successfully created child: always returns 0
vfork()因为如下两个特性而更具效率:
- 无需为子进程复制虚拟内存页或页表。子进程共享父进程的内存,直至其成功执行了exec()或是调用_exit()退出。
- 在子进程调用exec()或_exit()之前,将暂停执行父进程。
系统是在内核空间为每个进程维护文件描述符表,且在vfork()调用期间将复制该表,所以子进程对文件描述符的操作不会影响到父进程。
vfork()调用后,系统将保证子进程先于父进程获得调度以使用CPU。
#define _BSD_SOURCE /* To get vfork() declaration from <unistd.h> in case _XOPEN_SOURCE >= 700 */ #include "tlpi_hdr.h" int main(int argc, char *argv[]) { int istack = 222; switch (vfork()) { case -1: errExit("vfork"); case 0: /* Child executes first, in parent's memory space */ sleep(3); /* Even if we sleep for a while, parent still is not scheduled */ write(STDOUT_FILENO, "Child executing\n", 16); istack *= 3; /* This change will be seen by parent */ _exit(EXIT_SUCCESS); default: /* Parent is blocked until child exits */ write(STDOUT_FILENO, "Parent executing\n", 17); printf("istack=%d\n", istack); exit(EXIT_SUCCESS); } }
执行结果:
Child executing------执行然后睡眠3秒。 Parent executing-----父进程会一直挂起,知道子进程退出。 istack=666-----------子进程对istack的修改影响到了父进程。
除非速度绝对重要的场合,新程序应当舍vfork()而取fork()。
4. fork()之后的竞争条件
调用fork()之后,无法确定父子进程间谁将率先访问CPU。
#include <sys/wait.h> #include "tlpi_hdr.h" int main(int argc, char *argv[]) { int numChildren, j; pid_t childPid; if (argc > 1 && strcmp(argv[1], "--help") == 0) usageErr("%s [num-children]\n", argv[0]); numChildren = (argc > 1) ? getInt(argv[1], GN_GT_0, "num-children") : 1; setbuf(stdout, NULL); /* Make stdout unbuffered */ for (j = 0; j < numChildren; j++) { switch (childPid = fork()) { case -1: errExit("fork"); case 0: printf("child"); _exit(EXIT_SUCCESS); default: printf("parent"); wait(NULL); /* Wait for child to terminate */ break; } } exit(EXIT_SUCCESS); }
大部分情况下先执行父进程,使用如上程序测试在【Linux al-B250-HD3 4.15.0-117-generic #118~16.04.1-Ubuntu SMP】执行100万次,得到child优先执行791次。
因此,不应对fork()之后执行父子进程的特定顺序做任何假设。如有需要,则必须采用某种同步技术:信号量、文件锁、pipe、信号等。
如果fork()之后子进程立即执行exec(),那么父进程修改数据段和堆栈,就要为子进程复制将要修改的页,这一复制纯属浪费。此时,优先调度子进程决策更佳。
fork()之后,父进程在CPU中正处于活跃状态,并且其内存管理信息也备置于硬件内存管理单元的TLB中。此时,优先调度父进程决策更佳。
5. 同步信号以规避竞争条件
主进程调用fork()之后,必须等待子进程完成某系动作,通过信号完成等待同步。
#include <signal.h> #include "curr_time.h" /* Declaration of currTime() */ #include "tlpi_hdr.h" #define SYNC_SIG SIGUSR1 /* Synchronization signal */ static void /* Signal handler - does nothing but return */ handler(int sig) { } int main(int argc, char *argv[]) { pid_t childPid; sigset_t blockMask, origMask, emptyMask; struct sigaction sa; setbuf(stdout, NULL); /* Disable buffering of stdout */ sigemptyset(&blockMask); sigaddset(&blockMask, SYNC_SIG); /* Block signal */ if (sigprocmask(SIG_BLOCK, &blockMask, &origMask) == -1) errExit("sigprocmask"); sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; sa.sa_handler = handler; if (sigaction(SYNC_SIG, &sa, NULL) == -1) errExit("sigaction"); switch (childPid = fork()) { case -1: errExit("fork"); case 0: /* Child */ /* Child does some required action here... */ printf("[%s %ld] Child started - doing some work\n", currTime("%T"), (long) getpid()); sleep(2); /* Simulate time spent doing some work */ /* And then signals parent that it's done */ printf("[%s %ld] Child about to signal parent\n", currTime("%T"), (long) getpid()); if (kill(getppid(), SYNC_SIG) == -1) errExit("kill"); /* Now child can do other things... */ _exit(EXIT_SUCCESS); default: /* Parent */ /* Parent may do some work here, and then waits for child to complete the required action */ printf("[%s %ld] Parent about to wait for signal\n", currTime("%T"), (long) getpid()); sigemptyset(&emptyMask); if (sigsuspend(&emptyMask) == -1 && errno != EINTR) errExit("sigsuspend"); printf("[%s %ld] Parent got signal\n", currTime("%T"), (long) getpid()); /* If required, return signal mask to its original state */ if (sigprocmask(SIG_SETMASK, &origMask, NULL) == -1) errExit("sigprocmask"); /* Parent carries on to do other things... */ exit(EXIT_SUCCESS); } }
执行结果如下:
[16:59:14 29414] Parent about to wait for signal [16:59:14 29415] Child started - doing some work [16:59:16 29415] Child about to signal parent [16:59:16 29414] Parent got signal
6. 总结