多进程编程之概述
2、函数fork
一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同(pid ppid 和某些资源量和统计量等不同)的进程。
fork创建子进程时继承了父进程的数据段、代码段、栈、堆,注意从父进程继承来的是虚拟地址空间,同时页复制了页表(没有复制物理块)。因此,此时父子进程拥有相同的虚拟地址,映射的物理内存也是一致的。由于父进程和子进程共享物理页面,内核将其标记为“只读”,父子双方均无法对其修改。无论父进程还是子进程试图对一个共享页面执行写操作,就会产生一个错误,这时内核就把这个页复制到一个新的页面给这个进程,并标记“可写”,同时修改页表,把原来的只读页面标记为“可写”,留给另外一个进程使用-------写时拷贝技术。注意:内核在为子进程分配物理内存时,并没有将代码段对应的数据另外复制一份给子进程,最终父子进程代码段映射的是同一块物理内存(代码段在单个进程内部本来就是只读的)。fork()的实际开销就是复制父进程的也表以及给子进程创建唯一的进程描述符。
fork()函数调用一次,返回两次,它有三种不同的返回值:通过返回值来判断当前进程是子进程还是父进程。
1)在父进程中,fork函数返回新建子进程的id;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值。
1 #include <unistd> 2 3 pid_t fork(void); 4 返回值:子进程返回0,父进程返回子进程ID;若出错,返回负1。
1 #include <stdio.h> 2 #include <unistd.h> 3 4 int main(int argc, char * argv) 5 { 6 fork(); 7 fork() && fork() || fork(); 8 fork(); 9 return 0; 10 }
2)进程想执行另外一个程序。
1 /*知识点*/ 2 3 #include <unistd.h> 4 #include <stdio.h> 5 6 int main() 7 { 8 int var; 9 pid_t pid; 10 var = 88; 11 char buf[] = "this is a test\n"; 12 13 write(STDOUT_FILENO, buf, sizeof(buf) - 1); 14 printf("1111111111111111111\n"); 15 if ((pid = fork()) < 0) { 16 printf("fork error"); 17 }else if (pid == 0){ 18 var++; 19 }else { 20 sleep(2); 21 } 22 printf("--------------pid = %d, var = %d\n", pid, var); 23 return 0; 24 } 25 26 执行./a.out的输出结果是: 27 this is a test 28 1111111111111111111 29 --------------pid = 0, var = 89 30 --------------pid = 878, var = 88 31 32 执行./a.out > test,cat test的结果是: 33 this is a test 34 1111111111111111111 35 --------------pid = 0, var = 89 36 1111111111111111111 37 --------------pid = 895, var = 88 38 39 /*总结:write函数是不带缓冲的,fork之前调用,只写到标准输出一次;而标准I/O库是带缓冲的,如果标准输出连到终端设备,则它是行缓冲,否则它是全缓冲。
当(./a.out)运行该程序时,只得到printf输出一次,其原因是标准输出缓冲区由换行符冲洗。当将标准输出重定向到一个文件时,得到printf输出两次。
其原因是在fork之前调用了printf一次,但当调用fork时,该数据仍在缓冲区中,然后在将父进程数据空间复制到子进程中时,该缓冲区数据也被复制到子进程中。*/
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 int main() 5 { 6 int var = 8; 7 pid_t pid; 8 printf("before vfork\n"); 9 if((pid = vfork()) < 0){ 10 err_sys("vfork error"); 11 }else if (pid == 0){ 12 var++; 13 _exit(0); 14 } 15 printf("pid = %d var = %d\n",getpid(), var); 16 exit(0); 17 }
a. 在main函数中执行return,等效于在main函数中调用exit()函数;
b.调用exit()函数。此函数由ISO C定义,其操作包括调用各种终止处理程序,然后关闭所有的I/O流等。
c.调用_exit或_Exit函数。ISOC定义_Exit,其目的是提供一种无需运行终止处理程序或信号处理程序而终止的方法。在UNIX系统中,这两个函数并不冲洗I/O流。_exit函数是由exit调用。
d.进程的最后一个线程在其启动例程中执行return语句。但是该线程的返回值不用作进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态0返回。
e.进程的最后一个线程调用pthread_exit函数。同前面一样,该线程的返回值不用作进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态0返回。
(2)进程异常终止:
1 #include <stdio.h> 2 #include <stdlib.h> //exit 3 #include <unistd.h> //_exit 4 5 int main() 6 { 7 printf("this is a test\n"); 8 printf("again test"); 9 _exit(0); 10 }
1 #include <sys/wait.h> 2 3 pid_t wait (int *status); 4 pid_t waitpid(pid_t pid, int *status, int options); 5 6 /*两个函数返回值:若成功,返回进程id,并将子进程的终止状态存放在由status指向的存储单元中;若出错,返回 0 或 -1*/
宏 |
说明 |
WIFEXITED(status) | 若为正常终止子进程返回的状态,则为真。对于这种情况可执行WEXITSTATUS(status),取子进程传送给exit、_exit或_Exit参数的低8位 |
WIFSIGNALED(status) | 若为异常终止子进程返回的状态,则为真(接到一个不捕捉的信号)。对于这种情况,可执行WTERMSIG(status),取使子进程终止的信号编号。另外,有些实现定义宏WCOREDUMP(status),若已产生终止进程的core文件,则它返回真 |
WIFSTOPPED(status) | 若为当前暂停子进程的返回状态,则为真。对于这种情况,可执行WSTOPSIG(status),取使子进程暂停的信号编号 |
WIFCONTINUED(status) | 若在作业控制暂停后已经继续的子进程返回了状态,则为真。(POSIX.1的XSI扩展;仅用于waitpid。) |
1 while (getppid() != 1) 2 sleep(1); 3 /*这种形式的循环称为轮询,它的问题是浪费了CPU的时间,因为调用者每隔1s都被唤醒,然后进行条件测试*/
1 ret = waitpid(-1, NULL, WNOHANG | WUNTRACED); 2 /*如果不想使用它们,option可以设置为0*/ 3 ret = waitpid(-1, NULL, 0);
如果使用了WNOHANG(wait no hung)参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。WUNTRACED不经常用到,这里不做解释。
1 #include <unistd.h> 2 3 int execl( const char *pathname, const char *arg0, ... /* (char *)0 */ ); 4 5 int execv( const char *pathname, char *const argv[] ); 6 7 int execle( const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */ ); 8 9 int execve( const char *pathname, char *const argv[], char *const envp[] ); 10 11 int execlp( const char *filename, const char *arg0, ... /* (char *)0 */ ); 12 13 int execvp( const char *filename, char *const argv[] ); 14 15 int fexecve (int fd, char *const argv[], char *const envp[]); 16 17 /*7个函数返回值:若出错则返回-1,若成功则不返回值*/
18 /*这些函数之间的第一个区别是前4个函数取路径名作为参数,后两个函数则取文件名作为参数,最后一个取文件描述符作为参数*/
这7 个函数在函数名和使用语法的规则上都有细微的区别,下面就可执行文件查找方式、参数表传递方式及环境变量这几个方面进行比较说明。
① 查找方式:上表其中前4个函数的查找方式都是完整的文件目录路径,而后2个函数(也就是以p结尾的两个函数)可以只给出文件名,系统就会自动从环境变量“$PATH”所指出的路径中进行查找。
② 参数传递方式:exec函数族的参数传递有两种方式,一种是逐个列举的方式,而另一种则是将所有参数整体构造成指针数组进行传递。在这里参数传递方式是以函数名的第5位字母来区分的,字母为“l”(list)的表示逐个列举的方式,字母为“v”(vertor)的表示将所有参数整体构造成指针数组传递,然后将该数组的首地址当做参数传给它,数组中的最后一个指针要求是NULL。
③ 环境变量:exec函数族使用了系统默认的环境变量,也可以传入指定的环境变量。这里以“e”(environment)结尾的两个函数execle、execve就可以在envp[]中指定当前进程所使用的环境变量替换掉该进程继承的所以环境变量。
PATH环境变量:
PATH环境变量包含了一张目录表,系统通过PATH环境变量定义的路径搜索执行码;
查看PATH环境变量:export $PATH;
mac_fan$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
例1:execlp()
1 #include <stdio.h> 2 #include <unistd.h> 3 int main() 4 { 5 if(fork()==0){ 6 if(execlp("/usr/bin/env","env",NULL)<0) 7 { 8 perror("execlp error!"); 9 return -1 ; 10 } 11 } 12 return 0 ; 13 }
输出结果:
mac-fandeMacBook-Pro:me mac_fan$ /usr/bin/env
TERM_PROGRAM=Apple_Terminal
SHELL=/bin/bash
TERM=xterm-256color
TMPDIR=/var/folders/mx/48lcj5zn4cz8r61db79p5s1h0000gn/T/
例2:execle()
1 #include <unistd.h> 2 #include <stdio.h> 3 int main() 4 { 5 /*命令参数列表,必须以 NULL 结尾*/ 6 char *envp[]={"PATH=/tmp","USER=sun",NULL}; 7 if(fork()==0){ 8 /*调用 execle 函数,注意这里也要指出 env 的完整路径*/ 9 if(execle("/usr/bin/env","env",NULL,envp)<0) 10 { 11 perror("execle error!"); 12 return -1 ; 13 } 14 } 15 return 0 ; 16 }
输出结果:
mac_fan$ ./a.out
mac_fan$ PATH=/tmp
USER=sun
在exec函数族中,后缀l、v、p、e指定函数将具有某种操作能力:
后缀 | 操作能力 |
l | list希望接收以逗号分隔的参数列表,列表以NULL指针作为结束标志 |
v | vertor希望接收到一个以NULL结尾的字符串数组的指针 |
p | 是一个以NULL结尾的字符串数组指针,函数可以根据PATH变量查找子程序文件 |
e | 函数传递指定参数envp,可以不继承父进程的环境变量,改变子进程的环境,无后缀e时,子进程使用当前程序的环境 |
1 /*具体代码*/ 2 #ifdef HAVE_CONFIG_H 3 #include <config.h> 4 #endif 5 6 #include <stdio.h> 7 #include <stdlib.h> 8 #include <unistd.h> 9 #include <string.h> 10 #include <errno.h> 11 12 int main(int argc, char *argv[]) 13 { 14 //以NULL结尾的字符串数组的指针,适合包含v的exec函数参数 15 char *arg[] = {"ls", "-a", NULL}; 16 17 /** 18 * 创建子进程并调用函数execl 19 * execl 中希望接收以逗号分隔的参数列表,并以NULL指针为结束标志 20 */ 21 if( fork() == 0 ) 22 { 23 // in clild 24 printf( "1------------execl------------\n" ); 25 if( execl( "/bin/ls", "ls","-a", NULL ) == -1 ) 26 { 27 perror( "execl error " ); 28 exit(1); 29 } 30 } 31 32 /** 33 *创建子进程并调用函数execv 34 *execv中希望接收一个以NULL结尾的字符串数组的指针 35 */ 36 if( fork() == 0 ) 37 { 38 // in child 39 printf("2------------execv------------\n"); 40 if( execv( "/bin/ls",arg) < 0) 41 { 42 perror("execv error "); 43 exit(1); 44 } 45 } 46 47 /** 48 *创建子进程并调用 execlp 49 *execlp中 50 *l希望接收以逗号分隔的参数列表,列表以NULL指针作为结束标志 51 *p是一个以NULL结尾的字符串数组指针,函数可以DOS的PATH变量查找子程序文件 52 */ 53 if( fork() == 0 ) 54 { 55 // in clhild 56 printf("3------------execlp------------\n"); 57 if( execlp( "ls", "ls", "-a", NULL ) < 0 ) 58 { 59 perror( "execlp error " ); 60 exit(1); 61 } 62 } 63 64 /** 65 *创建子里程并调用execvp 66 *v 望接收到一个以NULL结尾的字符串数组的指针 67 *p 是一个以NULL结尾的字符串数组指针,函数可以DOS的PATH变量查找子程序文件 68 */ 69 if( fork() == 0 ) 70 { 71 printf("4------------execvp------------\n"); 72 if( execvp( "ls", arg ) < 0 ) 73 { 74 perror( "execvp error " ); 75 exit( 1 ); 76 } 77 } 78 79 /** 80 *创建子进程并调用execle 81 *l 希望接收以逗号分隔的参数列表,列表以NULL指针作为结束标志 82 *e 函数传递指定参数envp,允许改变子进程的环境,无后缀e时,子进程使用当前程序的环境 83 */ 84 if( fork() == 0 ) 85 { 86 printf("5------------execle------------\n"); 87 if( execle("/bin/ls", "ls", "-a", NULL, NULL) == -1 ) 88 { 89 perror("execle error "); 90 exit(1); 91 } 92 } 93 94 /** 95 *创建子进程并调用execve 96 * v 希望接收到一个以NULL结尾的字符串数组的指针 97 * e 函数传递指定参数envp,允许改变子进程的环境,无后缀e时,子进程使用当前程序的环境 98 */ 99 if( fork() == 0 ) 100 { 101 printf("6------------execve-----------\n"); 102 if( execve( "/bin/ls", arg, NULL ) == 0) 103 { 104 perror("execve error "); 105 exit(1); 106 } 107 } 108 return EXIT_SUCCESS; 109 }
在执行exec后,进程ID没有改变。但是新程序从调用进程继承了的下列属性:
进程ID和父进程ID、实际用户ID和实际组ID、附属组ID、进程组ID、会话ID、控制终端、闹钟尚余留的时间、当前工作目录、根目录、文件模式创建屏蔽字、文件锁、进程信号屏蔽、未处理信号、资源限制、nice值……
在exec前后实际用户ID和实际组ID保持不变,而有效ID是否改变则取决于所执行程序文件的设置用户ID为和设置组ID位是否设置。如果新程序的设置用户ID位已设置,则有效用户ID变成程序文件所有者的ID,否则有效用户ID不变。对ID组的处理方式与此相同。