fork函数可以从一个已存在的进程创建出一个新的进程。新进程为子进程,而原进程为父进程。
#include
pid_t fork(void);//pid_t为返回值
返回值:fork成功就把子进程pid返回给父进程,而把0返回给子进程,如果fork失败就把-1返回给父进程,子进程没有返回值
进程调用fork,
内核首先分配新的内存块和内核数据结构给子进程,将父进程部分数据结构拷贝给子进程,然后添加子进程到系统进程列表中,fork返回,开始调度器调度。
fork之后,父子进程谁先执行完全由调度器决定。
通常,父子进程代码共享,物理空间也是使用同一块,但任何一个进程尝试写入,操作系统先进行进程数据拷贝,让不同的进程数据进行分离,更改页表映射,然后再让进程进行修改—写实拷贝。
原因:系统中有太多的进程;实际用户的进程数超过了限制呀
这里有一段代码,运行后可以看到自己的操作系统最多可以容纳多少进程数。如果有虚拟机或者有云服务器的小伙伴可以试试噢,系统崩溃后退出系统等一会重启就好
1 #include2 #include3 4 int main()5 {6 int num=0;7 while(1) 8 {9 int ret=fork();10 if(ret<0)//如果创建子进程失败11 { 12 printf("fork error!,%d \n",num);13 break;14 } 15 else if(ret==0) 16 { 17 //子进程 18 while(1)19 sleep(1);20 21 }22 //父进程23 num++;24 }25 return 0;26 }
咱们写c++或者c代码时,大多数从main函数开始写写写,然后写完了就return 0;那么这个return 0有啥意义呢?所谓的return 0这个0就是进程退出码,退出码记录着进程退出的结果等等
进程退出的场景无非就三种
代码运行完毕,结果正确
代码运行完毕,结果不正确
代码运行异常终止了
那么当代码运行完毕后,结果在哪可以看到呢?
首先我写了这段代码,如果num等于5050那么main函数的进程退出码就是1,否则是0
然后运行后,第一次echo $?查看到就是mytest.c的main函数进程的退出码1.而echo $?也是进程,退出码是0,那么为什么后者进程的退出码是0呢?
return 0这个0标识着代码跑完了,进程执行的结果正确,而非0标识代码跑完了,结果不正确!
而!0里面不同的数字标识着不同的错误
这里要提到一个函数 strerror,该函数可以把进程退出码转化成相应的可以概括结果的字符串;然后这里我写一个小程序打印200以内的进程退出码对应的结果的字符串概括
我把结果拷贝到下面了,可以看到Linux系统下一共有134种进程退出码,每一个退出码都有对应的结果。其中第一种0代表着进程执行的结果正确,而其他都是进程执行的结果错误对应的原因。
0: Success1: Operation not permitted2: No such file or directory3: No such process4: Interrupted system call5: Input/output error6: No such device or address7: Argument list too long8: Exec format error9: Bad file descriptor10: No child processes11: Resource temporarily unavailable12: Cannot allocate memory13: Permission denied14: Bad address15: Block device required16: Device or resource busy17: File exists18: Invalid cross-device link19: No such device20: Not a directory21: Is a directory22: Invalid argument23: Too many open files in system24: Too many open files25: Inappropriate ioctl for device26: Text file busy27: File too large28: No space left on device29: Illegal seek30: Read-only file system31: Too many links32: Broken pipe33: Numerical argument out of domain34: Numerical result out of range35: Resource deadlock avoided36: File name too long37: No locks available38: Function not implemented39: Directory not empty40: Too many levels of symbolic links41: Unknown error 4142: No message of desired type43: Identifier removed44: Channel number out of range45: Level 2 not synchronized46: Level 3 halted47: Level 3 reset48: Link number out of range49: Protocol driver not attached50: No CSI structure available51: Level 2 halted52: Invalid exchange53: Invalid request descriptor54: Exchange full55: No anode56: Invalid request code57: Invalid slot58: Unknown error 5859: Bad font file format60: Device not a stream61: No data available62: Timer expired63: Out of streams resources64: Machine is not on the network65: Package not installed66: Object is remote67: Link has been severed68: Advertise error69: Srmount error70: Communication error on send71: Protocol error72: Multihop attempted73: RFS specific error74: Bad message75: Value too large for defined data type76: Name not unique on network77: File descriptor in bad state78: Remote address changed79: Can not access a needed shared library80: Accessing a corrupted shared library81: .lib section in a.out corrupted82: Attempting to link in too many shared libraries83: Cannot exec a shared library directly84: Invalid or incomplete multibyte or wide character85: Interrupted system call should be restarted86: Streams pipe error87: Too many users88: Socket operation on non-socket89: Destination address required90: Message too long91: Protocol wrong type for socket92: Protocol not available93: Protocol not supported94: Socket type not supported95: Operation not supported96: Protocol family not supported97: Address family not supported by protocol98: Address already in use99: Cannot assign requested address100: Network is down101: Network is unreachable102: Network dropped connection on reset103: Software caused connection abort104: Connection reset by peer105: No buffer space available106: Transport endpoint is already connected107: Transport endpoint is not connected108: Cannot send after transport endpoint shutdown109: Too many references: cannot splice110: Connection timed out111: Connection refused112: Host is down113: No route to host114: Operation already in progress115: Operation now in progress116: Stale file handle117: Structure needs cleaning118: Not a XENIX named type file119: No XENIX semaphores available120: Is a named type file121: Remote I/O error122: Disk quota exceeded123: No medium found124: Wrong medium type125: Operation canceled126: Required key not available127: Key has expired128: Key has been revoked129: Key was rejected by service130: Owner died131: State not recoverable132: Operation not possible due to RF-kill133: Memory page has hardware error134: Unknown error 134135: Unknown error 135136: Unknown error 136137: Unknown error 137
一般情况下,进程正常终止,要么是main函数返回(别的函数返回为函数调用结束);调用exit;系统调用_exit;
exit:调用这个函数后可以让进程直接退出 ,参数为进程退出码
然后我写了这么一个代码,如果main函数没退出则进入死循环。
那么运行后查看看到main函数确实是退出了
然后我再写这段代码,一般情况下其他函数结束为函数调用结束,而我在这个addtosum函数这里在返回值前面调用了个exit函数,看它是退出函数调用还是退出进程。
事实证明exit函数在任意函数调用都是直接退出进程!
exit是库函数,而_exit是系统调用,那么exit的底层实现也是 _exit,那么exit和 _exit有什么区别呢?
写了这段代码
运行后可以看到两秒后hello bug打印出来了
那么把exit改成_exit呢?
可以看到压根就没打印出来
这个对比可以知道,库函数的exit和系统调用的_eixt的区别是:exit在退出进程前会刷新缓冲区,而 _exit则不会。并且可以推断出缓冲区不在操作系统中,应该在用户空间里。
如果子进程退出,父进程对该进程不管不顾,那么就可能造成僵尸进程问题,进而导致内存泄露。另外进程一旦变成了僵尸进程,就连杀死进程的指令kill -9都无能为力。那么父进程应该怎么管理退出的子进程呢?
子进程运行完成后,父进程要通过进程等待的方式:回收子进程资源,获取子进程退出信息,这样过后就避免了僵尸进程的出现。
父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
第一个函数的参数为*status,是一个整数类型的指针,大多数情况都传NULL,如果成功就返回被收集子进程的pid,如果失败就返回-1
然后我写了这样的函数,创建了一个子进程,进到子进程里面打印子进程pid和父进程ppid,然后睡眠一秒,五秒后子进程退出,但由于父进程也睡眠了,所以会进入僵尸状态,之后几秒后父进程回收子进程并打印出wait的返回值
运行后看到确实如此
我写了这么一段代码,ret获取到子进程的pid,stastus获取到子进程的退出信息。
那么status到底是啥呢?
1.wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充;如果传递NULL,表示不关心子进程的退出状态信息;否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
2.status不能简单的当作整形来看待,可以当作位图来看待,即status要都能表示进程退出时的三种场景。(代码运行完毕,结果正确;代码运行完毕,结果不正确;代码运行异常终止了)
整数status的二进制下有32个比特位,现在先看前16个比特位(0-15);
第8-15个比特位(次低八位)存放进程的退出状态即子进程的退出码(通过退出状态得知进程运行完结果是否正确)退出码为0则结果正确。非0则对应错误的情况。
第0-6位(低七位)存放进程的终止信号(通过终止信号得知进程是否正常退出),第8位存放core dump标志(如图:status的二进制结构),终止信号为0—进程正常退出。非0为异常,通过kill -l可以看到大部分终止信号的情况。
通过status&0x7F[01111111]得到终止信号, 再通过(status>>8)&0xFF[011111111]得到退出码
相应的,通过kill杀死子进程也可以获取到相应的终止信号
比如我对子进程kill -3,那么子进程返回父进程的终止信号也是3
WIFEXITED(status) :若正常终止子进程返回的状态,则为真,(查看进程是否正常退出)
WEXITSTATUS(status) :若WIFEXITED非零,提取子进程退出码(查看进程的退出码)
1#include2 #include3 #include4 #include5 #include6 #include7 #include8 int main()9 {10 pid_t id=fork();11 assert(id!=-1);12 if(id==0)13{14 //子进程15 int num=30;16 while(num)17 {18 printf("child running,pid: %d ,ppid: %d ,num: %d\n",getpid(),getppid(),num--);19 sleep(1);20 }21 exit(10);22 }23 //父进程24 int status=0;25 int ret=waitpid(id,&status,0);26 if(ret>0)27 { 28 //判断子进程是否正常退出29 if(WIFEXITED(status))//子进程正常退出30 {//判断子进程运行的结果31 printf("exit code: %d\n",WEXITSTATUS(status));32 }else 33 {34 //子进程异常终止35 printf("child exit abnormally!\n ");36 }37 // printf("wait success,exit code: %d, sig: %d\n",(status>>8)&0xFF,status&0x7F);38 }39 return 0;40 }
当子进程退出时,会把代码和数据释放掉,但是退出信息(退出码和退出信号)要存在子进程的pcb中,此时子进程为Z状态,当父进程系统调用waitpid/wait时,父进程会通过子进程id从子进程pcb中拿退出信息到status里。
小帅—>父进程;女朋友—>子进程;打电话—>系统调用wait/waitpid
这个男人叫小帅,他有个女朋友,一天小帅约女朋友去看电影,约好了9点到女朋友楼下等她。小帅如期而至。但是没有看到女朋友身影,小帅打电话给女朋友,女朋友说在化妆要等一会,小帅说好阿,但是不要挂电话,小帅等阿等,过两分钟问女朋友一次化好了没,过两分钟又问,直到女朋友到了楼下才挂电话。
—不挂电话检测女朋友状态就是阻塞等待。
又一天,小帅约女朋友去吃饭,也是9点到楼下。小帅如期而至,打电话给女朋友,女朋友说在化妆要等一下,这次小帅说挂电话,等等在打电话给她。小帅在等等的时候一会看下球赛,一会看下书,过了一会打电话给女朋友问好了没,女朋友说还没好再等等。小帅就挂了电话继续等,等的时候做别的事情。
打电话—状态检测,如果没有就绪,立即返回(挂电话);每一次都是非阻塞等待。多次非阻塞等待—轮询。
把宏定义的WHOHANG传给waitpid,则为非阻塞等待。
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID,如果waitpid调用失败,则返回-1
#include2 #include3 #include4 #include5 #include6 #include7 #include8 int main()9 {10 pid_t id=fork();11 assert(id!=-1);12 if(id==0)13 {14 //子进程15 int num=3; 16 while(num)17 {18 printf("child running,pid: %d ,ppid: %d ,num: %d\n",getpid(),getppid(),num--);19 sleep(3);20 }21 exit(10);22 }23 //父进程24 int status=0;25 while(1)26 {27 pid_t ret=waitpid(id,&status,WNOHANG);//WHOHANG:非阻塞->子进程没有退出,父进程检测时候,立即返回28 if(ret==0)29 {30 //waitpid调用成功,子进程没有退出31 printf("wait done,but child is running...\n");32 }33 else if(ret>0)34 {35 //waitpid调用成功,子进程退出了36 printf("wait success,exit code: %d,sig: %d\n",(status>>8)&0xFF,status&0x7F);37 break;38 }39 else{40 //waitpid调用失败41 printf("waitpid call failed!\n");42 break;43 }44 sleep(1);45 }46 return 0;}
下面图是父进程非阻塞等待且轮询等待,最后子进程正常退出。
这里我传一个错误的id给父进程,即waitpid调用失败
可以看到打印了调用失败的情况,且父进程退出而子进程还在运行被OS领养
非阻塞等待的意义:不会占用父进程的所有资源,可以在轮询期间做别的事情。
在父进程非阻塞等待子进程期间,可以做其他事,我这里写了几个task,选择让父进程回调函数的方式执行。
1 #include2 #include3 #include4 #include5 #include6 #include7 #include8 9 #define NUM 1010 typedef void (*func_t)();//函数指针-void 是函数返回类型,fun_t是函数名,没有参数11 12 func_t handerTask[NUM];13 14 void task1()15 {16 printf("hander task1\n");17 }18 void task2()19 {20 printf("hander task2\n");21 }22 void task3()23 {24 printf("hander task3\n");25 }26 void loadTask()27 {28 memset(handerTask,0,sizeof(handerTask));29 handerTask[0]=task1;30 handerTask[1]=task2;31 handerTask[2]=task3;32 }33 int main()34 {35 pid_t id=fork();36 assert(id!=-1);37 if(id==0) 38 {39 //子进程40 int num=3;41 while(num)42 {43 printf("child running,pid: %d ,ppid: %d ,num: %d\n",getpid(),getppid(),num--);44 sleep(3);45 }46 exit(10);47 }48 //父进程49 loadTask();50 int status=0;51 while(1)52 {53 pid_t ret=waitpid(id,&status,WNOHANG);//WHOHANG:非阻塞->子进程没有退出,父进程检测时候,立即返回54 if(ret==0)55 {56 //waitpid调用成功,子进程没有退出57 printf("wait done,but child is running...\n");58 for(int i=0;handerTask[i]!=NULL;i++)59 {60 handerTask[i]();//采用回调的方式,执行我们想让父进程在非阻塞等待时做的事情。61 }62 63 64 65 }66 else if(ret>0)67 {68 //waitpid调用成功,子进程退出了69 printf("wait success,exit code: %d,sig: %d\n",(status>>8)&0xFF,status&0x7F);70 break;71 } 72 else{73 //waitpid调用失败74 printf("waitpid call failed!\n");75 break;76 }77 sleep(1);78 }79 return 0;}
程序替换的本质:将指定程序的代码和数据加载到指定的位置上,且覆盖之前的代码和数据。
进程通过pcb找到自己的虚拟空间,再通过页表找到物理空间执行自己的代码和数据,而程序替换通过exec函数把磁盘上的要替换的代码和数据加载到当前进程的物理内存中并覆盖,那么进程执行的就是后来的代码和数据拉!
此时只是替换代码和数据,没有创建新的进程。
这里有exec开头的函数,统称exec函数
函数调用成功—程序替换,调用失败—不替换
带l字符的函数—传路径,像list一样用next串起来
要执行哪一个程序:传程序的相对路径/绝对路径
要怎么执行:跟命令行输入一样:“程序名”,“选项1”,“选项2”…,NULL【exec函数都要以NULL结尾】
可变参数列表:给函数传递不同个数的参数
execl函数如果调用失败了就返回-1,如果成功了就不返回,即便成功了原先后面的代码都会被替换掉,所以返回了也没有用处。
Q:子进程在调用exec函数进行程序替换时,会影响父进程吗?
A:此时父进程和子进程共享同一块代码和数据,当子进程调用exec函数时,操作系统会给子进程进行写时拷贝,然后再进行程序替换。父子进程互不影响,这也体现了进程的独立性。
这样体现了创建子进程的目的:
1.让子进程执行父进程的一部分。
2.让子进程进行程序替换,执行一个全新的程序。
带p字符的函数,都只需传程序名即可
一样能跑起来
那么如果execl和execlp函数放在同一个函数,两者重复吗?
前者是通过路径传参,后者是通过环境变量PATH寻找函数名,不重复。
那如何让exec函数调用自己写的程序呢?
这里我写了一些标识性代码
通过创建伪目标all,来达到同时执行多个目标文件。
现在我想要myexec去调用我写的mycom程序
1 #include 2 #include3 #include 4 #include 5 #include6 #include7 int main()8 { 9 10 printf("process is running...\n");11 pid_t id=fork();12 assert(id!=-1);13 if(id==0) 14 { 15 //子进程 16 sleep(1); 17 // char*const argv_[]={"ls","-a","-l","--color=auto",NULL};18 execl("./mycom","mycom",NULL);19 exit(1); 20 //execvp("ls",argv_); 21 //execv("/usr/bin/ls",argv_);22 // execl("/user/bin/ls/"/*传程序路径*/,"ls","-a","-l","--color=auto",NULL/*想怎么执行*/);23 // execlp("ls"/*传程序名*/,"ls","-a","-l","--color=auto",NULL/*想怎么执行*/);24 // //全部的exec函数参数都是以NULL结尾25 26 } 27 int status=0;28 pid_t ret=waitpid(id,&status,0);29 if(id>0) 30 {31 printf("wait success:%d ,sig number: %d,child exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);32 } 33 printf("process running done...\n");34 return 0;35 36 }
且可以使用程序替换,调用任何后端语言对应的可执行程序。
在mycom.c文件调用PATH,PWD环境变量,和一个MYENV的自定义变量
然后myexec.c里的execle函数调用自定义变量
可以看到环境变量没调用到但是自定义变量调用到了
这次execle函数调用环境变量
可以看到环境变量被调用了,但是自定义变量没有被调用
那么即想调用自定义变量也想调用环境变量呢?
putenv:把自定义变量导入环境变量的表里
把自定义变量MYENV加入到环境变量表里
main函数有命令行参数,参数有程序,环境变量等等,那么是先调用main函数还是先加载各种命令行参数到内存中呢?
先加载!因为main函数也要被传参!命令行参数和环境变量等等先加载到内存,如果函数需要就传参!
实际上,上面提到的各种exec函数都是系统调用execve的封装,而各种封装后也是为了适用各种应用场景。
好了,这里小结一下,重点讲解了进程终止:进程退出的三种情形,查看进程退出码,进程终止的方法;进程等待:进程等待的方法,如何获取子进程进程状态和退出码,使用宏定义查看进程是否正常退出;阻塞等待和非阻塞等待的介绍和区别;进程替换:五种进程替换函数的使用,putenv函数的使用等等。这篇写了好几天,制作不易,求点赞~~~
上一篇:人大金仓数据库KSQL常用命令
下一篇:Java高手速成 | 多态性实战