fork()函数用于进程创建,定义如下:
| 所需头文件 | #include #include |
| 函数原型 | pid_t fork(void); |
| 函数返回值 | 0: 子进程 大于0的整数为子进程的PID:父进程 -1:错误 |
fork()函数示例代码:
#include#include #include int main(void) { pid_t pid; //保存fork函数返回值 printf("本进程的PID = %dn",getpid()); pid = fork(); if(pid == -1) { printf("error fork() n"); } else if(pid > 0) { //父进程 printf("pid = %d, father id = %d n",pid,getpid()); } else { //子进程 printf("pid = %d, chile id = %d n",pid,getpid()); } return 0; }
运行结果如下:
从上图可以看出,首先进程执行打印了本进程的pid = 4596,随后执行fork()函数创建子进程,由于fork()函数有两个返回值分别对应父进程和子进程,因此加上if条件判断。
在父进程中打印了pid返回值4597,和父进程pid=4596,fork的返回值4597对应的就是子进程的pid.
在子进程中打印了pid返回值0,和子进程pid = 4597。
使用fork函数得到的子进程从父进程的继承了整个进程的地址空间,包括:进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等。
调用fork()后,子进程如果完全拷贝了父进程的数据段,堆,栈内存。(通常在:fork()之后,子进程往往会立马调用exec系列的函数,来执行一个新的不同的程序),那么会产生较高的性能开销。因为如果子进程拷贝父进程的数据段,堆,栈内存,那么又会立马调用exec系列函数覆盖掉刚刚拷贝的内存,那么这么做是毫无意义的。因此Linux引进了写时拷贝(copy - on - write)的技术。写时拷贝可以避免拷贝大量根本就不会使用的数据。因此可以看出写时拷贝极大提升了Linux系统下fork函数运行地性能。
写时拷贝指的是子进程的页表项指向与父进程相同的物理页,这也只需要拷贝父进程的页表项就可以了,不会复制整个内存地址空间,同时把这些页表项标记为只读。如果父子进行都不对页面进行操作,那么便一直共享同一份物理页面。只要父子进程有一个尝试进行修改某一个页面,那么就会发生缺页异常(page fault)。那么内核便会为该页面创建一个新的物理页面,并将内容复制到新的物理页面中,让父子进程真正地各自拥有自己的物理内存页面,并将页表中相应地页表项标记为可写。
写时拷贝父子进程修改某一个页面前后变化如下图所示:
示例代码:
#include#include #include #include int main() { pid_t pid; int cnt=0; printf("cnt=%dn",cnt); pid = fork(); if(pid >0){ //父进程 sleep(1); printf("cnt=%dn",cnt); printf("this is father print pid is %dn",getpid()); } else if(pid == 0){ //子进程 printf("this is child print pid is =%dn",getpid()); cnt++; } return 0; }
运行结果如下:
可以看出子进程修改cnt值,其实修改的是内核新创建的物理页面。
二、exec函数族如果一个进程想执行另一个程序,那么它就可以调用fork函数新建一个进程,然后调用任何一个exec函数使子进程重生。exec函数通常在fork()函数创建的子进程中调用,可以实现去执行新的程序,清空原来的堆栈区、代码区和数据区,让新的可执行程序完全替代现有的资源
语法定义如下:
所需头文件 #include函数原型 int execl(const char *path, const char *arg, ...); int execv(const char *path, char *const argv[]); int execle(const char *path, const char *arg, ..., char *const envp[]); int execve(const char *path, char *const argv[], char *const envp[]); int execlp(const char *file, const char *arg, ...); int execvp(const char *file, char *const argv[]); 函数返回值 -1:出错
① 第一个参数是带路径的执行码(execlp、execvp函数第一个参数是无路径的,系统会根据PATH自动查找然后合成带路径的执行码);
② 第二个是不带路径的执行码,执行码可以是二进制执行码和Shell脚本。
调用举例如下:
#include#include #include #include #include int main(int argc, char *argv[]) { //以NULL结尾的字符串数组的指针,适合包含v的exec函数参数 char *arg[] = {"ls", "-a", NULL}; if( fork() == 0 ) { // in clild printf( "1------------execl------------n" ); if( execl( "/bin/ls", "ls","-a", NULL ) == -1 ) { perror( "execl error " ); exit(1); } } if( fork() == 0 ) { // in child printf("2------------execv------------n"); if( execv( "/bin/ls",arg) < 0) { perror("execv error "); exit(1); } } if( fork() == 0 ) { // in clhild printf("3------------execlp------------n"); if( execlp( "ls", "ls", "-a", NULL ) < 0 ) { perror( "execlp error " ); exit(1); } } if( fork() == 0 ) { printf("4------------execvp------------n"); if( execvp( "ls", arg ) < 0 ) { perror( "execvp error " ); exit( 1 ); } } if( fork() == 0 ) { printf("5------------execle------------n"); if( execle("/bin/ls", "ls", "-a", NULL, NULL) == -1 ) { perror("execle error "); exit(1); } } if( fork() == 0 ) { printf("6------------execve-----------n"); if( execve( "/bin/ls", arg, NULL ) == 0) { perror("execve error "); exit(1); } } return 0; }
结果如下:



