目录
12、父进程通过wait/waitpid可以拿到子进程的退出结果,为什么要用wait/waitpid?直接使用全局变量不行吗?
13、既然进程是独立的,进程退出码,不也是子进程的数据吗?父进程凭什么能够拿到?
我们知道进程 = 内核数据结构 + 进程代码和数据
操作系统会分配新的内存块和内核数据结构给子进程,将父进程部分数据结构内容拷贝至子进程,添加子进程到系统进程列表中,fork返回,开始调度器调度(这时fork并没有执行完)
创建子进程,给子进程分配对应的内核结构必须是子进程私有的,子进程也要有代码和数据,可是一般而言,子进程没有加载的过程,也就是说子进程没有自己的代码和数据,子进程只能使用父进程的代码和数据。
对于父子进程的代码:都是不可写的,只能读取,所以可以父子共享
数据:不需要将不会被访问的或者只会读取的数据拷贝一份
拷贝:将来的父进程或者是子进程写入的数据
一般而言即便是操作系统也无法提前知道哪些空间被写入,即使提前拷贝了,并不会立马使用,所以就会有写时拷贝技术,将父子进程分离,这是高效使用内存的表现。
创建子进程就相当于浅拷贝,一份父进程给子进程页表指向相同的物理地址,当修改变量时使用深拷贝。fork之后父子进程代码共享是所有的代码都共享。
我们的代码汇编之后,会有很多行代码,而且每行代码加载到内存之后,都有对应的地址。
因为进程随时有可能被中断(可能并没有执行完),下次回来,还必须从之前的位置继续运行,就要求CPU必须随时记录程序执行的位置,所以CPU内有对应的寄存器数据用来记录当前进程的执行位置,EIP有的教程又会叫他PC,它是程序计数器,用来记录当前正在执行代码的下一行代码的地址。
CPU实际上特别的傻,它只会做三件事,取指令,分析指令,执行指令
当创建子进程时父进程的上下文数据也会给子进程,虽然父子进程各自调度,各自会修改EIP,但是已经不重要了,因为子进程已经认为自己的EIP起始值就是fork之后的代码。
写时拷贝技术使父子进程彻底分离,保证了进程独立性
写时拷贝的数据在父子进程中就不再是只读的了,两者相互分离互不干扰。
1、父进程复制自己,使父子进程执行不同的代码段(子承父业)
2、一个进程要执行不同的程序(父子进行不同的事情)
1、系统中有太多的进程
2、用户的进程超过了限制(一个普通的用户创建的进程是有限的)
创建task_struct进程地址空间mm_struct创建页表,构建映射关系,加载代码和数据
释放进程申请的相关数据结构和对应的数据和代码
1、代码跑完,结果正确
2、代码跑完,结果错误
3、代码没有跑完,程序崩溃
我们经常在C\C++程序中写return 0,但是return 0是什么意思?可以返回其它值吗?
main的返回值是进程退出码
可以使用echo $?来获取最近一个进程执行完毕的退出码
如果退出码是0结果是正确,非0代表运行结果不正确
- 1 #include
- 2 #include
- 3
- 4 int main()
- 5 {
- 6 printf("Hello World\n");
- 7
- 8 return 0;
- 9 }
-
因为我们的代码在main中return的是0,所以test的退出码就是0
如果我们return 10 时看看会发生什么?
它的退出码就是10
main函数的返回值的意义是返回给上一级进程用来评判该进程执行的结果。
- 1 #include
- 2 #include
- 3
- 4 int sum(int top)
- 5 {
- 6 int ans = 0;
- 7 for(int i = 0; i <= top; i++)
- 8 {
- 9 ans += i;
- 10 }
- 11
- 12 return ans;
- 13 }
- 14
- 15 int main()
- 16 {
- 17 int ret = 0;
- 18 int ans = sum(100);
- 19 if(ans != 5050)
- 20 {
- 21 ret = 1;
- 22 }
- 23 return ret;
- 24 }
我们知道从1一直加到100的和是5050,我们就可以通过返回值来判断结果是否正确。
如果返回0就代表执行结果正确,返回1就代表结果不正确。
它的退出码是0,代表结果正确,如果我们故意在for中去掉=
非0值有无数个,不同的非0值可以表示不同的错误原因
给我们的程序在运行结束之后,结果是不正确的,方便定位错误的原因细节。
退出码只是针对代码跑完了,结果不正确的情况,注意一定是代码跑完了。
我们也可以使用库函数strerror来打印具体的退出码的含义
- 1 #include
- 2 #include
- 3
- 4 int main()
- 5 {
- 6 for(int i = 0; i < 150; i++)
- 7 {
- 8 printf("%d: %s\n", i, strerror(i));
- 9 }
- 10
- 11 return 0;
- 12 }
打印的结果是?
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
138: Unknown error 138
139: Unknown error 139
140: Unknown error 140
141: Unknown error 141
142: Unknown error 142
143: Unknown error 143
144: Unknown error 144
145: Unknown error 145
146: Unknown error 146
147: Unknown error 147
148: Unknown error 148
149: Unknown error 149
0表示成功,非0表示错误
既然我们已经知道了不同退出码的含义但是它们是正确的吗?
我们使用ls命令去查看一个不存在的目录,它的退出码是2, 我们通过和上面的退出码结果比对
2: No such file or directory答案是正确的。
我们再看一个例子:
我们使用9号信号杀掉一个根本不存在的进程,它的退出码是1 ,可是退出码1是1: Operation not permitted,与我们的预期不一样,这是因为不同的程序可以自定义退出码,但是退出码0一定是程序正常运行完,且答案正确。
我们再做一个实验
- int* ptr = NULL;
- *ptr = 30;
我们知道它会出现野指针问题,那么它的退出码是什么呢?
我们一看是139,可是可是根本没有139号退出码,这是怎么回事?
程序崩溃的时候,退出码无意义,一般而言退出码对应的return语句并没有被执行就被操作系统将进程杀掉。
在main函数内,我们可以return + 进程退出码 来退出进程。
而在其他函数中要使用exit函数来退出
exit的参数status就是退出码。exit在任何地方调用都直接表示终止进程。
- 1 #include
- 2 #include
- 3
- 4 int sum(int top)
- 5 {
- 6 int ans = 0;
- 7 for(int i = 0; i <= top; i++)
- 8 {
- 9 ans += i;
- W> 10 }
- 11
- 12 exit(1);
- 13 }
- 14
- 15 int main()
- 16 {
- 17 int ans = sum(100);
- 18 return 0;
- 19 }
退出码是1跟我们所想的一致。
还有一个函数也可以退出进程,但是它们之间略有差别
_exit,它也可以退出进程,我们将上面的exit全部换成_exit它们的现象是一样的
结论是一样的,但是他们之间还是有不同的。
- 1 #include
- 2 #include
- 3 #include
- 4
- 5 int main()
- 6 {
- 7 printf("you can see me");
- 8
- 9 sleep(1);
- 10 exit(1);
- 11
- 12 return 0;
- 13 }
这段代码比较简单,我们知道它会打印you can see me,然后光标闪1秒
但是如果我们去掉\n会发生什么呢?
发现好像跟加了\n就只有换行的区别
但是如果我们将exit换成_exit时会发生什么呢?
发现光标闪动1秒然后没有打印出语句。
这是因为调用_exit程序是直接退出的,并没有刷新缓冲区
这里也就引出来一个问题,printf——\n数据是保存在缓冲区中的,请问这个缓冲区在哪里?缓冲区是谁维护的?
这个缓冲区一定不在操作系统内部,它是由C标准库所维护的
子进程退出,父进程不管子进程,子进程要处于僵尸状态,会导致内存泄漏
父进程创建了子进程,是要让子进程办事,那么子进程把任务完成的怎么样?父进程需要关心,那么如何得知?
子进程完成任务就三种情况:
代码跑完,结果正确
代码跑完,结果不正确
代码没有跑完,程序崩溃
父进程就通过进程等待的方式来获取子进程的相关信息。
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
接下来我们创建一个子进程,然后让子进程变成僵尸进程
- 1 #include
- 2 #include
- 3 #include
- 4
- 5 int main()
- 6 {
- 7 pid_t id = fork();
- 8
- 9 if(id < 0)
- 10 {
- 11 perror("fork");
- 12 exit(1);
- 13 }
- 14 else if(id == 0)
- 15 {
- 16 //子进程
- 17 int cnt = 5;
- 18 while(cnt--)
- 19 {
- 20 printf("I am child %d pid: %d ppid: %d \n", cnt, getpid(), getppid());
- 21 }
- 22
- 23 }
- 24 else
- 25 {
- 26 //父进程
- 27 while(1)
- 28 {
- 29
- 30 printf("I am father pid: %d ppid: %d \n", getpid(), getppid());
- 31 }
- 32 }
- 33
- 34 return 0;
- 35 }
我们可以使用wait和waitpid来回收僵尸进程
wait有一个参数status,它的含义与waitpid的status一样,在后面会讲解
它会监测子进程,直到子进程的进程状态发生变化
如果成功会返回子进程的pid,如果失败会返回-1
- 1 #include
- 2 #include
- 3 #include
- 4 #include
- 5 #include
- 6
- 7 int main()
- 8 {
- 9 pid_t id = fork();
- 10
- 11 if(id < 0)
- 12 {
- 13 perror("fork");
- 14 exit(1);
- 15 }
- 16 else if(id == 0)
- 17 {
- 18 //子进程
- 19 int cnt = 5;
- 20 while(cnt--)
- 21 {
- 22 printf("I am child %d pid: %d ppid: %d \n", cnt, getpid(), getppid());
- 23 sleep(1);
- 24 }
- 25
- 26 }
- 27 else
- 28 {
- 29 //父进程
- 30
- 31 pid_t ret = wait(NULL);
- 32 if(ret > 0)
- 33 {
- 34 printf("等待子进程成功,ret: %d",ret);
- 35 }
- 36 }
- 37 return 0;
这样就回收了子进程这个僵尸进程。
接下来我们再看一下waitpid这个函数
waitpid这个函数它有三个参数
第一个参数是pid 我们通常会给它赋两种值 -1 等待任意子进程与wait等效:pid > 0等待其进程id与pid相等的子进程
第三个参数options默认为0,表示阻塞式等待
第二个参数是status:输出型参数,标识子进程的退出结果
status并不是按照整数整体来使用的
按照比特位的方式,将32个比特位进行划分
正常终止时,我们看退出状态也就是退出码,通过次低8位
被信号所杀我们看低7位
那么如何拿到退出码呢?
(status >> 8) & 0xFF就获取了退出码
被信号所杀
status & 0x7F
进程异常退出,或者崩溃本质是操作系统杀掉了你的进程
操作系统通过发送信号的方式杀掉你的进程
进程异常退出或者崩溃使用status的低7位来查看
如果收到的信号是0,那么进程是正常的,这时退出码才会有意义
程序异常不光是内部代码有问题,也可能是外力直接杀掉,程序可能跑完了,可能没有跑完,所以退出码是无意义的
进程具有独立性,那么数据要发生写时拷贝,父进程无法拿到子进程全局变量的值
wait/waitpid是有这个权力的,它通过系统调用,来获取子进程的task_struct结构体里面的exit_code和exit_signal来获取
以上就是今天要讲的内容,本文仅仅简单介绍了fork及僵尸进程。