fork()
用来创建进程fork(void)
在linux中所有进程都是由init进程直接或间接创建
成功:在父进程中将返回子进程的PID;子进程返回0,以区别父进程
失败:父进程中返回-1
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/types.h> 4 5 int main(int argc,char *argv[]) 6 { 7 pid_t pid; 8 if((pid=fork())==-1) 9 printf("fork error"); 10 printf("bye\n"); 11 printf("当前进程的进程号pid:%d\n当前进程的父进程号ppid:%d\n",getpid(),getppid()); 12 return 0; 13 }
结果:
[[email protected] PCB]# ps aux
root 3905 0.0 0.1 108468 1904 pts/0 S Dec17 0:00 bash
[[email protected] PCB]# ./fork
bye
bye
当前进程的进程号pid:4570
当前进程的父进程号ppid:3905
pid=fork()中pid的值:4571 //在父进程中将返回子进程的PID
当前进程的进程号pid:4571
当前进程的父进程号ppid:4570
pid=fork()中pid的值:0 //子进程返回0,以区别父进程
子进程中的代码在fork返回位置执行;子进程创建成功之后,和父进程同时执行,竞争系统资源,谁先执行由调度算法决定。
父子进程
子进程会复制父进程的几乎所有信息:子进程复制父进程用户空间所有数据;
子进程复制父进程内核空间PCB中绝大多数数据;
一、文件流缓冲区的资源位于用户空间,所以全部复制。即如果流缓冲区中有临时信息,都会复制到子进程的用户空间流缓冲区中。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 5 int main(int argc,char *argv[]) 6 { 7 pid_t pid; 8 printf("在fork之前,有回车\n"); 9 printf("在fork之前,没有回车,getpid()——pid=%d\t",getpid()); 10 pid=fork(); 11 if(pid==0) 12 printf("\nfork后创建的子进程getpid()——pid=%d\n",getpid()); 13 else 14 printf("\nfork后创建的父进程getpid()——pid=%d\n",getpid()); 15 }
[[email protected] PCB]# ./streamfork
在fork之前,有回车
在fork之前,没有回车,getpid()——pid=5536
fork后创建的父进程getpid()——pid=5536
在fork之前,没有回车,getpid()——pid=5536
fork后创建的子进程getpid()——pid=5537
按照上面所说,子进程要在fork方法执行并返回某值后才会复制代码到子进程,子进程从返回值位置向后执行,不会执行之前的代码,但这段代码却输出了之前的代码,这就是复制了缓冲区的缘故。
之所以出现两次输出,有两方面原因,首先是跟printf的缓冲机制有关,我们在前面说过printf("%d",i)=fprintf(stdout,"%d",i),就是说printf函数输出某些内容时,操作系统仅仅是把该内容放到了stdout的缓冲队列里了,并没有立刻写到屏幕上。但是,只要看到有/n 则会立即刷新stdout,才能够马上打印了。
其次就是因为复制了缓冲区。由于父进程在fork前输出的第二个printf函数时没有回车,而输出流是带缓冲的,从而该信息缓存到用户空间,在fork创建子进程后,系统为子进程复制父进程数据空间以及标准输出缓冲区,子进程刷新了输出缓冲区,将数据输出。
二、子进程复制父进程的数据段,BSS段,代码段,堆空间,栈空间,文件描述符,但是对于文件描述符关联的内核文件表项(即struct file结构体)则是采用共享的方式
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <fcntl.h> 6 #include <sys/types.h> 7 8 int main(int argc,char *argv[]) 9 { 10 pid_t pid; 11 int fd; 12 int i=1; 13 int status; 14 char *ch1="hello"; 15 char *ch2="world"; 16 char *ch3="IN"; 17 if((fd=open("test.txt",O_RDWR|O_CREAT,0644))==-1) 18 { 19 perror("parent open"); 20 exit(EXIT_FAILURE); 21 } 22 if(write(fd,ch1,strlen(ch1))==-1) 23 { 24 perror("parent write"); 25 exit(EXIT_FAILURE); 26 } 27 if((pid=fork())==-1) 28 { 29 perror("fork"); 30 exit(EXIT_FAILURE); 31 } 32 else if(pid==0) 33 { 34 i=2; 35 printf("in chile\n"); 36 printf("i=%d\n",i); 37 if(write(fd,ch2,strlen(ch2))); 38 perror("chile write"); 39 return 0; 40 } 41 else 42 { 43 sleep(1);//等待子进程先执行 44 printf("in parent\n"); 45 printf("i=%d\n",i); 46 if(write(fd,ch3,strlen(ch3))); 47 perror("parent write"); 48 wait(&status);//等待子进程结束 49 return 0; 50 } 51 }
[[email protected] PCB]# ./forkfilrstruct
in chile
i=2
chile write: Success
//在这里明显等待1s才出现in parent,即sleep()让父进程等待1s好让子进程完成写ch2的操作,1s后再写ch3
in parent
i=1
parent write: Success
[[email protected] PCB]# cat test.txt
helloworldIN
从test.txt的内容可以看出,父子进程对同一个文件操作,写入数据也不覆盖,即说明父子进程共享文件偏移,因此共享文件表项
而从变量i可以看出子进程赋值后父进程的i值不变,说明父子进程各自拥有这一变量的副本,互相不影响。
这里对wait函数稍加介绍:
wait(等待子进程中断或结束)
wait()会暂时停止进程的执行,直到有信号来到或子进程结束。如果在调用wait()时子进程已经结束,则wait()会立即返回子进程结束状态值,由参数status
返回,而子进程的进程识别码也会一起返回。如果不在意结束状态值,则参数status 可以设成NULL。
vfork()
vfolk()创建新进程时不复制父进程的地址空间,而是在必要的时候才申请新的存储空间,共享父进程的代码以及数据段等
1 #include<unistd.h> 2 #include<error.h> 3 #include<sys/types.h> 4 #include<stdio.h> 5 #include<stdlib.h> 6 int glob=6; //已初始化全局变量,存放在数据段 7 int main() 8 { 9 int var; 10 pid_t pid; 11 var=88; //局部变量,存放在栈 12 printf("in beginning:\tglob=%d\tvar=%d\n",glob,var); 13 if((pid=vfork())<0) 14 { 15 perror("vfork"); 16 exit(EXIT_FAILURE); 17 } 18 else if(pid==0) 19 { 20 printf("in child,modify the var:glob++,var++\n"); 21 glob++; 22 var++; 23 printf("in child:\tglob=%d\tvar=%d\n",glob,var); 24 _exit(0); 25 } 26 else 27 { 28 printf("in parent:\tglob=%d\tvar=%d\n",glob,var); 29 return 0; 30 } 31 }
输出的glob,var的值相同,均是自加之后的结果,说明子进程修改后父进程跟着改变,即两者共享。
若vfork改成fork,则子进程是自加后的结果,父进程不变,说明子进程是父进程的一份复制。
但是由于父子空间共享内存空间,使得由子函数调用vfork创建的子进程(架设子进程为先执行函数的进程)调用其它函数或运行其他程序后会,父进程会出现段错误,如下:
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<stdlib.h> 4 void test() 5 { 6 pid_t pid; 7 pid=vfork();//创建子进程 8 if(pid==-1) 9 { 10 perror("vfork"); 11 exit(EXIT_FAILURE); 12 } 13 else if(pid==0) //子进程先运行 14 { 15 printf("1:child pid=%d,ppid=%d\n",getpid(),getppid()); 16 return; 17 } 18 else 19 printf("2:parent pid=%d,ppid=%d\n",getpid(),getppid()); 20 } 21 void fun() 22 { 23 int i; 24 int buf[100]; 25 for(i=0;i<100;i++) 26 buf[i]=0; 27 printf("3:child pid=%d,ppid=%d\n",getpid(),getppid()); 28 } 29 int main() 30 { 31 pid_t pid; 32 test(); 33 fun(); 34 }
1.创建主函数,申请栈空间(局部变量、返回值、参数等)
2.调用test函数,申请test函数的栈空间
3.test函数创建子进程,子进程先运行,在test中输出pid和ppid,清理栈空间
4.子进程调用fun函数,覆盖原来test函数的栈空间,执行完毕后退出
5.父进程从返回处开始执行,可是栈已经不存在了
所以如果希望在创建的子进程中运行新的程序,则用fork()函数创建子进程,再用exec系列函数替代子进程用户空间的资源(代码、堆、栈等),内核信息基本不修改。
execl系列
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/types.h> 4 5 int main(int argc,char* argv[]) 6 { 7 pid_t pid; 8 if((pid=fork())<0) 9 { 10 printf("error"); 11 } 12 else if(pid==0) 13 { 14 execl("/bin/ls","ls","-l","/home",(char *)0); 15 } 16 else 17 printf("father ok!\n"); 18 19 }
[[email protected] task]# ./execl
father ok!
[[email protected] task]# 总用量 16
drwxr-xr-x. 4 root root 4096 11月 14 23:10 df
drwx------. 29 hadoop hadoop 4096 9月 4 23:15 hadoop
drwx------. 44 sun sun 4096 12月 30 04:45 sun
drwxr-xr-x. 12 root root 4096 12月 30 05:01 test
在执行execl系列函数时,默认情况下,新代码可以使用原来代码中打开的文件描述符,即执行execl时,并不关闭进程原来打开的文件
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/types.h> 4 #include <fcntl.h> 5 #include <string.h> 6 #include <stdlib.h> 7 8 int main(int argc,char* argv[]) 9 { 10 int fd,status; 11 pid_t pid; 12 fd=open("test.txt",O_RDWR|O_APPEND|O_CREAT,0644);//打开文件,产生一个文件描述符fd,从文件尾开始追加 13 if(fd==-1) 14 { 15 perror("open"); 16 exit(EXIT_FAILURE); 17 } 18 printf("before child process write\n"); 19 system("cat test.txt");//创建新进程,在新进程中运行命令,直到新进程运行结束在运行父进程 20 if((pid=fork())==-1) 21 { 22 perror("fork"); 23 exit(EXIT_FAILURE); 24 } 25 if(pid==0) 26 { 27 char buf[128]; 28 sprintf(buf,"%d",fd);//将文件描述符写入缓冲区 29 execl("./newcode","newcode",buf,(char *)0);//执行newcode,把文件描述符以参数的形式传递给代码newcode,在newcode中执行对文件的追加写入工作 30 } 31 else 32 { 33 wait(&status); 34 printf("after child_process write\n"); 35 system("cat test.txt"); 36 } 37 }
1 #include <stdio.h> 2 #include <string.h> 3 #include <unistd.h> 4 5 int main(int argc,char* argv[]) 6 { 7 int i; 8 int fd; 9 char *ptr="helloworld\n"; 10 fd=atoi(argv[1]);//argv[1]中的值是写入buf的fd=open("test.txt",O_RDWR|O_APPEND|O_CREAT,0644) 11 i=write(fd,ptr,strlen(ptr));//写入fd关联的test.txt中,执行成功,说明原来的文件描述符可以使用 12 if(i<=0) 13 perror("write"); 14 close(fd); 15 }
[[email protected] task]# ./system_execl
before child process write
我是测试文件
after child_process write
我是测试文件
helloworld
父子进程共享文件描述符:此说法,其实是父子进程共享 文件表项(父进程和子进程共享同一个file table entry)
由于子进程是父进程的拷贝,子进程会拷贝父进程的进程描述符中的文件描述符表,可以说继承父进程的文件描述字(files_struct中的struct file *fd_array[NR_OPEN_DEFAULT]的拷贝)
如果我们没有调用exec函数,则我们父子进程的代码段,堆栈,数据段都完全相同(因为是拷贝),所以此时我们的子进程可以使用fork()之前的fd值,虽然此时fd是属于子进程的数据段(他是之前fd的拷贝)
一个进程一旦调用exec类函数,它本身就"死亡"了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。(不过exec类函数中有的还允许继承环境变量之类的信息。)
所以进程的存储映像被新程序代替(也就是说,含有对方地址的套接字地址结构也丢失了),但是由于即便exec之后,打开的文件描述符依然存在,所以我们可以通过调用getpeername(fd.....)获得对端的ip和端口号
一般来说:调用exec之前在进程中打开的描述字在exec之后还是保持打开状态的,我们可以通过fcntl函数设置FD_CLOEXEC描述字标志来关闭。此又称为文件描述字标签,默认情况是清除的
由于exec之后,原来connfd描述字肯定没有了,所以我们必须还原这个connfd
1、把connfd当成一个字符串,作为exec的命令行参数给新程序
2、在调用exec之前,把某个描述字设置成connfd,(通过close(fd),然后调用dup(connfd),则根据规则,使用最小未被使用的fd,及是fd,这样fd就和connfd一样指向同一个文件表项),通常我们用0,1,2设置成connfd。
在C程序中,文件由文件指针或者文件描述符表示。ISO C的标准I/0库函数(fopen, fclose, fread, fwrite, fscanf, fprintf等)使用文件指针,UNIX的I/O函数(open, close, read, write, ioctl)使用文件描述符。下面重点来说下,文件描述符是如何工作的。
文件描述符相当于一个逻辑句柄,而open,close等函数则是将文件或者物理设备与句柄相关联。句柄是一个整数,可以理解为进程特定的文件描述符表的索引。先介绍下面三个概念,后面讲下open、close等操作以后,文件和文件描述符产生什么关系,以及fork后文件描述符的继承等问题。
文件描述符表:用户区的一部分,除非通过使用文件描述符的函数,否则程序无法对其进行访问。对进程中每个打开的文件,文件描述符表都包含一个条目。
系统文件表:为系统中所有的进程共享。对每个活动的open, 它都包含一个条目。每个系统文件表的条目都包含文件偏移量、访问模式(读、写、or 读-写)以及指向它的文件描述符表的条目计数。
内存索引节点表: 对系统中的每个活动的文件(被某个进程打开了),内存中索引节点表都包含一个条目。几个系统文件表条目可能对应于同一个内存索引节点表(不同进程打开同一个文件)。
1、举例: 执行myfd = open( "/home/lucy/my.dat", O_RDONLY); 以后,上述3个表的关系原理图如下:
系统文件表包含一个偏移量,给出了文件当前的位置。若2个进程同时打开一个文件(如上图A,B)做读操作,每个进程都有自己相对于文件的偏移量,而且读入整个文件是独立于另一个进程的;如果2个进程打开同一个文件做写操作,写操作是相互独立的,每个进程都可以重写另一个进程写入的内容。
如果上面进程在open以后又执行了close()函数,操作系统会删除文件描述符表的第四个条目和系统文件表的对应条目(若指向它的描述符表唯一),并对内存索引节点表条目中的计数减1,如果自减以后变为0,说明没有其他进程链接此文件,将索引节点表条目也删除,而这里进程B也在open这个文件,所以索引节点表条目保留。
2、文件描述符的继承
通过fork()创建子进程时,子进程继承父进程环境和上下文的大部分内容的拷贝,其中就包括文件描述符表。
(1)对于父进程在fork()之前打开的文件来说,子进程都会继承,与父进程共享相同的文件偏移量。如下图所示(0-1-2 表示 标准输入-输出-错误):
系统文件表位于系统空间中,不会被fork()复制,但是系统文件表中的条目会保存指向它的文件描述符表的计数,fork()时需要对这个计数进行维护,以体现子进程对应的新的文件描述符表也指向它。程序关闭文件时,也是将系统文件表条目内部的计数减一,当计数值减为0时,才将其删除。
(2)相反,如果父进程先进程fork,再打开my.dat,这时父子进程关于my.dat
的文件描述符表指向不同的系统文件表条目,也不再共享文件偏移量(fork以后2个进程分别open,在系统文件表中创建2个条目);但是关于标准输入,标准输出,标准错误,父子进程还是共享的。