文章目录
- 进程控制
- 浅谈fork
- 写时拷贝
- fork调用失败的原因
- 进程终止
- 进程退出的场景
- 进程常见退出方法
- 查看进程退出码
- echo $? :查看进程退出码
- exit和_exit
- 进程等待
- 进程等待的方法
- wait
- waitpid
- 获取子进程status
- 宏定义查看进程是否正常退出,查看退出码
- 再谈僵尸进程
- 浅谈阻塞等待和非阻塞等待
- 宏定义WNOHANG:非阻塞等待
- 进程程序替换
- 替换函数
- execl
- execlp
- execv
- execvp
- execle
- putenv
- 先加载还是先调用函数?
进程控制
浅谈fork
fork函数可以从一个已存在的进程创建出一个新的进程。新进程为子进程,而原进程为父进程。
#include<unistd.h>
pid_t fork(void);//pid_t为返回值
返回值:fork成功就把子进程pid返回给父进程,而把0返回给子进程,如果fork失败就把-1返回给父进程,子进程没有返回值
进程调用fork,
内核首先分配新的内存块和内核数据结构给子进程,将父进程部分数据结构拷贝给子进程,然后添加子进程到系统进程列表中,fork返回,开始调度器调度。
fork之后,父子进程谁先执行完全由调度器决定。
写时拷贝
通常,父子进程代码共享,物理空间也是使用同一块,但任何一个进程尝试写入,操作系统先进行进程数据拷贝,让不同的进程数据进行分离,更改页表映射,然后再让进程进行修改—写实拷贝。
fork调用失败的原因
原因:系统中有太多的进程;实际用户的进程数超过了限制呀
这里有一段代码,运行后可以看到自己的操作系统最多可以容纳多少进程数。如果有虚拟机或者有云服务器的小伙伴可以试试噢,系统崩溃后退出系统等一会重启就好
1 #include<stdio.h>
2 #include<unistd.h>
3
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就是进程退出码,退出码记录着进程退出的结果等等
进程退出的场景
进程退出的场景无非就三种
代码运行完毕,结果正确
代码运行完毕,结果不正确
代码运行异常终止了
那么当代码运行完毕后,结果在哪可以看到呢?
进程常见退出方法
查看进程退出码
echo $? :查看进程退出码
首先我写了这段代码,如果num等于5050那么main函数的进程退出码就是1,否则是0
然后运行后,第一次echo $?查看到就是mytest.c的main函数进程的退出码1.而echo $?也是进程,退出码是0,那么为什么后者进程的退出码是0呢?
return 0这个0标识着代码跑完了,进程执行的结果正确,而非0标识代码跑完了,结果不正确!
而!0里面不同的数字标识着不同的错误
这里要提到一个函数 strerror,该函数可以把进程退出码转化成相应的可以概括结果的字符串;然后这里我写一个小程序打印200以内的进程退出码对应的结果的字符串概括
我把结果拷贝到下面了,可以看到Linux系统下一共有134种进程退出码,每一个退出码都有对应的结果。其中第一种0代表着进程执行的结果正确,而其他都是进程执行的结果错误对应的原因。
0: Success
1: Operation not permitted
2: No such file or directory
3: No such process
4: Interrupted system call
5: Input/output error
6: No such device or address
7: Argument list too long
8: Exec format error
9: Bad file descriptor
10: No child processes
11: Resource temporarily unavailable
12: Cannot allocate memory
13: Permission denied
14: Bad address
15: Block device required
16: Device or resource busy
17: File exists
18: Invalid cross-device link
19: No such device
20: Not a directory
21: Is a directory
22: Invalid argument
23: Too many open files in system
24: Too many open files
25: Inappropriate ioctl for device
26: Text file busy
27: File too large
28: No space left on device
29: Illegal seek
30: Read-only file system
31: Too many links
32: Broken pipe
33: Numerical argument out of domain
34: Numerical result out of range
35: Resource deadlock avoided
36: File name too long
37: No locks available
38: Function not implemented
39: Directory not empty
40: Too many levels of symbolic links
41: Unknown error 41
42: No message of desired type
43: Identifier removed
44: Channel number out of range
45: Level 2 not synchronized
46: Level 3 halted
47: Level 3 reset
48: Link number out of range
49: Protocol driver not attached
50: No CSI structure available
51: Level 2 halted
52: Invalid exchange
53: Invalid request descriptor
54: Exchange full
55: No anode
56: Invalid request code
57: Invalid slot
58: Unknown error 58
59: Bad font file format
60: Device not a stream
61: No data available
62: Timer expired
63: Out of streams resources
64: Machine is not on the network
65: Package not installed
66: Object is remote
67: Link has been severed
68: Advertise error
69: Srmount error
70: Communication error on send
71: Protocol error
72: Multihop attempted
73: RFS specific error
74: Bad message
75: Value too large for defined data type
76: Name not unique on network
77: File descriptor in bad state
78: Remote address changed
79: Can not access a needed shared library
80: Accessing a corrupted shared library
81: .lib section in a.out corrupted
82: Attempting to link in too many shared libraries
83: Cannot exec a shared library directly
84: Invalid or incomplete multibyte or wide character
85: Interrupted system call should be restarted
86: Streams pipe error
87: Too many users
88: Socket operation on non-socket
89: Destination address required
90: Message too long
91: Protocol wrong type for socket
92: Protocol not available
93: Protocol not supported
94: Socket type not supported
95: Operation not supported
96: Protocol family not supported
97: Address family not supported by protocol
98: Address already in use
99: Cannot assign requested address
100: Network is down
101: Network is unreachable
102: Network dropped connection on reset
103: Software caused connection abort
104: Connection reset by peer
105: No buffer space available
106: Transport endpoint is already connected
107: Transport endpoint is not connected
108: Cannot send after transport endpoint shutdown
109: Too many references: cannot splice
110: Connection timed out
111: Connection refused
112: Host is down
113: No route to host
114: Operation already in progress
115: Operation now in progress
116: Stale file handle
117: Structure needs cleaning
118: Not a XENIX named type file
119: No XENIX semaphores available
120: Is a named type file
121: Remote I/O error
122: Disk quota exceeded
123: No medium found
124: Wrong medium type
125: Operation canceled
126: Required key not available
127: Key has expired
128: Key has been revoked
129: Key was rejected by service
130: Owner died
131: State not recoverable
132: Operation not possible due to RF-kill
133: Memory page has hardware error
134: Unknown error 134
135: Unknown error 135
136: Unknown error 136
137: Unknown error 137
一般情况下,进程正常终止,要么是main函数返回(别的函数返回为函数调用结束);调用exit;系统调用_exit;
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就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
第一个函数的参数为*status,是一个整数类型的指针,大多数情况都传NULL,如果成功就返回被收集子进程的pid,如果失败就返回-1
然后我写了这样的函数,创建了一个子进程,进到子进程里面打印子进程pid和父进程ppid,然后睡眠一秒,五秒后子进程退出,但由于父进程也睡眠了,所以会进入僵尸状态,之后几秒后父进程回收子进程并打印出wait的返回值
运行后看到确实如此
waitpid
我写了这么一段代码,ret获取到子进程的pid,stastus获取到子进程的退出信息。
那么status到底是啥呢?
获取子进程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#include<string.h>
2 #include<stdio.h>
3 #include<unistd.h>
4 #include<assert.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7 #include<stdlib.h>
8 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点到楼下。小帅如期而至,打电话给女朋友,女朋友说在化妆要等一下,这次小帅说挂电话,等等在打电话给她。小帅在等等的时候一会看下球赛,一会看下书,过了一会打电话给女朋友问好了没,女朋友说还没好再等等。小帅就挂了电话继续等,等的时候做别的事情。
打电话—状态检测,如果没有就绪,立即返回(挂电话);每一次都是非阻塞等待。多次非阻塞等待—轮询。
宏定义WNOHANG:非阻塞等待
把宏定义的WHOHANG传给waitpid,则为非阻塞等待。
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID,如果waitpid调用失败,则返回-1
#include<string.h>
2 #include<stdio.h>
3 #include<unistd.h>
4 #include<assert.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7 #include<stdlib.h>
8 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 #include<string.h>
2 #include<stdio.h>
3 #include<unistd.h>
4 #include<assert.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7 #include<stdlib.h>
8
9 #define NUM 10
10 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函数
函数调用成功—程序替换,调用失败—不替换
execl
带l字符的函数—传路径,像list一样用next串起来
要执行哪一个程序:传程序的相对路径/绝对路径
要怎么执行:跟命令行输入一样:“程序名”,“选项1”,“选项2”…,NULL【exec函数都要以NULL结尾】
可变参数列表:给函数传递不同个数的参数
execl函数如果调用失败了就返回-1,如果成功了就不返回,即便成功了原先后面的代码都会被替换掉,所以返回了也没有用处。
Q:子进程在调用exec函数进行程序替换时,会影响父进程吗?
A:此时父进程和子进程共享同一块代码和数据,当子进程调用exec函数时,操作系统会给子进程进行写时拷贝,然后再进行程序替换。父子进程互不影响,这也体现了进程的独立性。
这样体现了创建子进程的目的:
1.让子进程执行父进程的一部分。
2.让子进程进行程序替换,执行一个全新的程序。
execlp
带p字符的函数,都只需传程序名即可
一样能跑起来
那么如果execl和execlp函数放在同一个函数,两者重复吗?
前者是通过路径传参,后者是通过环境变量PATH寻找函数名,不重复。
execv
execvp
那如何让exec函数调用自己写的程序呢?
这里我写了一些标识性代码
通过创建伪目标all,来达到同时执行多个目标文件。
现在我想要myexec去调用我写的mycom程序
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<assert.h>
5 #include<sys/wait.h>
6 #include<sys/types.h>
7 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 }
且可以使用程序替换,调用任何后端语言对应的可执行程序。
execle
在mycom.c文件调用PATH,PWD环境变量,和一个MYENV的自定义变量
然后myexec.c里的execle函数调用自定义变量
可以看到环境变量没调用到但是自定义变量调用到了
这次execle函数调用环境变量
可以看到环境变量被调用了,但是自定义变量没有被调用
那么即想调用自定义变量也想调用环境变量呢?
putenv
putenv:把自定义变量导入环境变量的表里
把自定义变量MYENV加入到环境变量表里
先加载还是先调用函数?
main函数有命令行参数,参数有程序,环境变量等等,那么是先调用main函数还是先加载各种命令行参数到内存中呢?
先加载!因为main函数也要被传参!命令行参数和环境变量等等先加载到内存,如果函数需要就传参!
实际上,上面提到的各种exec函数都是系统调用execve的封装,而各种封装后也是为了适用各种应用场景。
好了,这里小结一下,重点讲解了进程终止:进程退出的三种情形,查看进程退出码,进程终止的方法;进程等待:进程等待的方法,如何获取子进程进程状态和退出码,使用宏定义查看进程是否正常退出;阻塞等待和非阻塞等待的介绍和区别;进程替换:五种进程替换函数的使用,putenv函数的使用等等。这篇写了好几天,制作不易,求点赞~~~