A Little Bit Process API fork() #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main (int argc, char *argv[]) { int x=0 ; printf ("hello world (pid:%d)\n" , (int )getpid()); int rc = fork(); if (rc < 0 ) { fprintf (stderr , "fork failed\n" ); exit (1 ); } else if (rc == 0 ) { printf ("hello, I am child (pid:%d) by (ppid:%d)---%p\n" , (int ) getpid(),(int )getppid(),&x); } else { printf ("hello, I am parent of %d (pid:%d)---%p\n" , rc, (int ) getpid(),&x); } return 0 ; }
grxer@grxer ~/D/s/O/o/cpu-api> ./p1 hello world (pid:5116 ) hello, I am parent of 5117 (pid:5116 ) ---0x7ffd842ab520 hello, I am child (pid:5117 ) by (ppid:1 ) ---0x7ffd842ab520 grxer@grxer ~/D/s/O/o/cpu-api> ./p1 hello world (pid:5164 ) hello, I am parent of 5165 (pid:5164 ) ---0x7ffd2fc1d450 hello, I am child (pid:5165 ) by (ppid:5164 ) ---0x7ffd2fc1d450
man 2 fork
#include <sys/types.h> #include <unistd.h> pid_t fork (void ) ;
调用fork()的进程被称为父进程,fork()会创建一个子进程,子进程不会从 main()函数开始执行,而是直接从 fork()系统调用返回
子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
fork的实质就是得到父进程在内核里描述进程信息的数据结构的副本,然后给一个新的pid
fork的子进程和父进程采用了一种叫做写时复制(copy-on-write )的技术,子进程直接复制了页表,他们刚开始映射到了同一块物理内存,刚开始物理地址和虚拟地址父子都相同,任何一个进程要对共享的页面“写操作”,内核会复制一个物理页面给这个进程使用,同时修改页表映射,copy on write同样存在于进程之间共享内存的场景
对于实际实现上可能会把原来可写页面标记为只读,并把页面引用计数加一,当有进程写入时会触发异常,操作系统根据引用计数来确定是COW,复制一份写入,并把页面引用计数减一
RETURN VALUE On success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately.
通过返回值可以进行不同处理
有趣的时我们上面两次结果的ppid是不同的,这是因为CPU调度程序(scheduler)决定了某个时刻哪个进程被执行,例如上一章的IO
进程1(init进程)没有父进程,是所有进程的祖先进程,如果子进程的父进程在子进程结束之前终止了,那么子进程的父进程ID就会变为1,即init进程的PID。
wait man 2 wait
进程结束后,内核并不是立即把它从系统中清除,被保持在一种已终止的状态中,直到被它的父进程回收(reaped)。被叫做僵死进程,如果父进程先结束了,虽然init进程会继承他们并回收,我们最好还是让父进程去做
#include <sys/types.h> #include <sys/wait.h> pid_t waitpid (pid_t pid,int *stateusp,int options=0 ) pid_t wait (int *statusp) ;waitpid的简单版本 相当于waitpid(- 1 ,*statusp, 0 ) 返回:如果成功,则为子进程的 PID, 如果出错,则为 -1 。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main (int argc, char *argv[]) { printf ("hello world (pid:%d)\n" , (int ) getpid()); int rc = fork(); if (rc < 0 ) { fprintf (stderr , "fork failed\n" ); exit (1 ); } else if (rc == 0 ) { printf ("hello, I am child (pid:%d)\n" , (int ) getpid()); sleep(1 ); } else { int wc = wait(NULL ); printf ("hello, I am parent of %d (wc:%d) (pid:%d)\n" , rc, wc, (int ) getpid()); } return 0 ; }
grxer@grxer ~/D/s/O/o/cpu-api> ./p2 hello world (pid:5404) hello, I am child (pid:5405) hello, I am parent of 5405 (wc:5405) (pid:5404)
两个进程运行先后顺序还是取决于cpu调度
exec man 3 exec
exec 函数族
#include <unistd.h> extern char **environ;int execl (const char *pathname, const char *arg, ... ) ;int execlp (const char *file, const char *arg, ... ) ;int execle (const char *pathname, const char *arg, ... ) ;int execv (const char *pathname, char *const argv[]) ;int execve (const char *pathname, char *const argv[], char *const envp[]) int execvp (const char *file, char *const argv[]) ;int execvpe (const char *file, char *const argv[], char *const envp[]) ;
只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数,函数功能大同小异就是调用加载器根据要加载的ELF头表里的信息覆写调用程序 的代码段(以及静态数据),堆、栈及其他内存空间也会被重新初始化,并把参数传递,该函数调用成功不会返回
execve实际上也不会把磁盘里的程序拷贝到物理内存,而是把进程的页表映射到磁盘上,触发缺页后由处理程序再加载到物理内存供cpu 使用
被继承
文件描述符
信号处理程序
用户和组 ID
环境变量
信号屏蔽字
当前进程中的任何线程或锁不会被继承,在新程序中,不会有任何父进程中的局部变量、栈和堆等内存信息。
int execve(const char *pathname, char *const argv[], char *const envp[])
filename:指向可执行文件名的用户空间指针。
argv:参数列表,指向用户空间的参数列表起始地址
envp:环境变量表,环境变量是一系列键值对,字符串类型
argv envp数组以null结尾
int main (int argc, char *argv [], char *envp [])
#include <stdio.h> #include <stdlib.h> int main (int argc, char * argv[],char *envp[]) { for (int j = 0 ; j < argc; j++) printf ("argv[%d]: %s\n" , j, argv[j]); for (size_t i = 0 ; envp[i]; i++) { printf ("%s\n" , envp[i]); } exit (EXIT_SUCCESS); } #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main (int argc, char *argv[]) { char *newargv[] = { NULL , "hello" , "world" , NULL }; char *newenviron[] = { "grxer=666" }; if (argc != 2 ) { fprintf (stderr , "Usage: %s <file-to-exec>\n" , argv[0 ]); exit (EXIT_FAILURE); } newargv[0 ] = argv[1 ]; execve(argv[1 ], newargv, newenviron); perror("execve" ); exit (EXIT_FAILURE); }
grxer@grxer ~/D/s/test> gcc -g -o myecho myecho.c && gcc -g -o execve execve.c grxer@grxer ~/D/s/test> ./execve ./myecho argv[0]: ./myecho argv[1]: hello argv[2]: world grxer=666 ./myecho hello world
用奇怪的约定做一些神奇的事情 fork和exev看起来是非常奇怪的,为什么要把两个操作分开去创建新进程,而不是整合成一个api,其实这给了shell 在fork 之后 exec 之前运行代码的机会,来实现一些很有趣的事
比如说我们的输出重定向
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #include <assert.h> #include <sys/wait.h> int main (int argc, char *argv[]) { int rc = fork(); if (rc < 0 ) { fprintf (stderr , "fork failed\n" ); exit (1 ); } else if (rc == 0 ) { close(STDOUT_FILENO); open("./p4.output" , O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU); char *myargs[3 ]; myargs[0 ] = strdup("wc" ); myargs[1 ] = strdup("p4.c" ); myargs[2 ] = NULL ; execvp(myargs[0 ], myargs); } else { int wc = wait(NULL ); assert(wc >= 0 ); } return 0 ; }
grxer@grxer ~/D/s/O/o/cpu-api> ./p4 grxer@grxer ~/D/s/O/o/cpu-api> cat ./p4.output 34 114 918 p4.c
可以看到我们成功把输出重定向到文件
原理:
fork后子进程会继承父进程默认打开的标准输入、标准输出和标准错误输出0、1 和 2 ,默认情况下,这些文件描述符与终端设备关联,因此,当程序从标准输入读取数据时,数据会从终端输入;当程序向标准输出写入数据时,数据会输出到终端。
我们在exec前把标准输出文件描述符给关闭,然后再打开./p4.output时,UNIX 系统从 0 开始寻找可以使用的文件描述符,自然找到之前被关闭的标准输出文件描述符,给替换掉,实现重定向
strace看一下 -ff filename 将不同进程(子进程)产生的系统调用输出到filename.PID文件
grxer@grxer ~/D/s/O/o/cpu-api> strace -ff -o task ./p4 grxer@grxer ~/D/s/O/o/cpu-api> ls ./task* ./task.7615 ./task.7616 close(1) = 0 openat(AT_FDCWD, "./p4.output", O_WRONLY|O_CREAT|O_TRUNC, 0700) = 1 newfstatat(1, "", {st_mode=S_IFREG|0777, st_size=17, ...}, AT_EMPTY_PATH) = 0 write(1, " 34 114 918 p4.c\n", 17) = 17
The openat() system call operates in exactly the same way as open(2)