当前位置: 首页 > news >正文

6. 进程控制

 补充一下写时拷贝中的一个细节:数据在拷贝后全都设为只读,未来要修改时,只读的数据要写入操作,碰到权限问题,此时不做异常处理,而是发生写时拷贝。

之前我们都是创建一个进程,如果我想创建多个进程呢?

也是可以的:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>#define N 10void runChild()
{int cnt = 5;while(cnt){printf("i am child:%d, ppid:%d\n", getpid(), getppid());sleep(1);cnt--;}
}int main()
{for(int i=0; i<N; i++){pid_t id = fork();if(id == 0){runChild();exit(0);}}return 0;
}

我这个进程一直创建子进程,创建出来如果是子进程就运行,如果是父进程就不做任何操作,循环回去继续创建。

1. 进程终止

我们的main函数总会有一个返回值,为什么总会return 0?1?-1?
这个值返回给谁了呢?
为什么要返回这个值?

        return 的这个值其实就是进程的退出码,表征进程的运行结果是否正确,0代表运行成功。如果我们运行这个我们写的main函数,这个值会被bash拿到。

        为什么会被bash拿到,因为进程中谁会关心我们运行进程的情况呢?一般而言就是我们运行进程的父进程会关心!父进程要把运行情况返回给用户,所以最终是用户关心进程的运行情况,也就是我们想知道运行的结果!

        若运行的结果不正确,我们想知道为什么不正确,这时就可以用return的不同返回值数字代表不同的出错原因--这也就是退出码

所以,main函数的返回值的本质:进程完成运行时是否是正确的结果,如果不是,可以用不同的数字表示不同的出错原因。

怎么拿到最近一个进程的退出码:echo $?

第二个是0是因为上一个执行的进程echo执行成功,所以它的退出码是0。

让我们通过下面的代码看一下所有错误码与其对应描述:

int main()
{for(int i=0; i<200; i++){printf("%d:%s\n", i, strerror(i));}
}
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

验证一下文件不存在但是想访问的情况:

可以看到系统提供的错误码和描述之间是一一对应的,我们可不可以自己设计一套退出码体系呢?当然可以。后面再说吧。

C语言还会引入一个全局变量errno,与退出码功能类似。

int main()
{int ret = 0;char* p = (char*)malloc(1000*1000*1000*4);if(p == NULL){printf("malloc fail, %d : %s\n", errno, strerror(errno));ret = errno;}else{printf("malloc success\n");}return ret;
}

进程退出的场景有三种:

  • 代码异常终止
  • 代码运行完毕,结果正确
  • 代码运行完毕,结果错误

一个代码结束时,肯定是先关心有无异常,然后再看运行结果是否正确。

        代码异常终止时,本质可能就是代码没有跑完,这时进程的退出码没有意义,我们不关心退出码了,但我们还得关心为什么异常,以及发生了什么异常?

进程出现异常,本质是我们的进程收到了对应的信号,什么信号呢?

就是kill中的各种信号。

一个一直在运行的进程,我们kill -几 进程pid,最后显示出来的就是以那种方式终止。

既然exit()里的值和return返回的值都可以做退出码,他们的区别是什么?

  • return 只能表示当前函数返回
  • exit 可以在任意地方被调用,都表示进程直接退出

exit 和 _exit

int main()
{printf("what can i say");sleep(1);exit(11);
}

1s后打印what can i say

int main()
{printf("what can i say");sleep(1);_exit(11);
}

只是把exit替换成了_exit,就不打印了?

_exit 退出时,没有打印缓冲区中的数据。所以他们的区别:

内核上面的是用户区,exit()是库函数,_exit()是系统调用_exit直接退出,什么都不干,但是exit会刷新缓冲区等等,搞完之后才会调用_exit退出。

所以我们 printf 写入的缓冲区绝对不在内核中

在main中,return n 等价于 exit(n),因为main运行时会将main的返回值当作exit 的参数。

2. 进程等待

之前说过,子进程退出,如果父进程还在运行而且对子进程不管不顾,不处理子进程返回的信息,那么就会形成僵尸进程,造成内存泄漏。

父进程怎么回收这个Z状态的子进程呢?通过进程等待,回收子进程资源,获取子进程退出信息。

进程等待是什么:

        通过系统调用接口 wait/waitpid,来对子进程进行状态检查和回收的功能。

为什么要有进程等待?

        僵尸进程无法被杀死,需要通过进程等待来处理掉他,进而解决内存泄漏问题--必须解决的
        我们需要通过进程等待,获取子进程的退出情况,父进程需要知道布置给子进程的任务完成的怎么样--要么关心,要么不关心,这是可选的。

wait:

int main()
{pid_t id = fork();if(id < 0){perror("fork");return 1;}else if(id == 0){int cnt = 5;while(cnt){printf("i am child, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(0);}else{int cnt = 10;while(cnt){printf("i am father, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}pid_t ret = wait(NULL);if(ret == id){printf("wait success, ret:%d\n", ret);}sleep(5);}return 0;
}

        子进程9669结束后,父进程还存在,然后wait,wait是等待任意一个子进程退出,返回它等待到的退出的这个子进程的pid,如果wait的返回值和传给父进程的id(子进程pid)一样,说明等待成功。

学到目前为止,进程等待是必须的!

上面是单个进程的情况,如果有多个进程呢?父进程创建了那么多子进程,都得回收,怎么回收呢

#define N 10void runChild()
{int cnt = 5;while(cnt){printf("i am child:%d, ppid:%d\n", getpid(), getppid());sleep(1);cnt--;}
}int main()
{for(int i=0; i<N; i++){pid_t id = fork();if(id == 0){runChild();exit(0);}printf("create child process:%d success\n", id);}sleep(5);for(int i=0; i<N; i++){pid_t id = wait(NULL);if(id > 0){printf("wait %d success\n", id);}}return 0;
}

其实直接搞一个循环,让他等待任意一个进程退出就好。

如果没有一个子进程退出呢?也就是父进程在wait时,子进程都不退,则会在wait处阻塞等待。

​#define N 10void runChild()
{int cnt = 5;while(1){printf("i am child:%d, ppid:%d\n", getpid(), getppid());sleep(1);cnt--;}
}int main()
{for(int i=0; i<N; i++){pid_t id = fork();if(id == 0){runChild();exit(0);}printf("create child process:%d success\n", id);}sleep(5);for(int i=0; i<N; i++){pid_t id = wait(NULL);if(id > 0){printf("wait %d success\n", id);}}return 0;
}

waitpid:

pid:

  • pid = -1:等待任意一个子进程,与wait等效
  • pid > 0:等待进程id和pid相等的子进程(某一子进程)

stat_loc:

  • NULL:不关心子进程退出状态信息

options:

  • 0:阻塞等待
  • WNOHANG:非阻塞轮询

waitpid(-1, NULL,0)和wait(NULL)是一样的效果。

pid很好理解,我们详细介绍其他两个参数

stat_loc:

stat_loc,共有32位,我们只考虑其中的16位。

前面提到了,子进程退出一共就3种退出场景,父进程等待,期望获得子进程退出时的哪些信息呢?

首先是子进程代码是否异常,一旦代码异常,就会被kill的信号所杀,这个信号就是kill -l中的各种信号:

“第几个信号”导致进程异常退出,这个退出信息存在32位的哪里呢?

可以看到低7位存放的就是异常信号信息。由于kill中没有0号信号,要检测当前进程是否异常,只用检测低7位是否为0就可以,若为0,说明进程没有收到kill信号,没有异常。

判断完有无异常,就要判断程序运行的是否正确了,这我们在进程终止中讲过,是通过退出码与其对应信息拿到的,8-15位就存的是退出码。这个core dump暂时不讲。

    pid_t ret = waitpid(id, &status, 0);if(ret == id){//7F = 0111 111printf("wait success, ret:%d, exit sig:%d, exit code:%d\n", ret, status&0x7F, (status>>8)&0xF);

这样就可以看到退出码和终止信号。

父进程通过waitpid读取子进程的PCB,检查子进程若为僵尸状态,则把这两个exit信息合并位status,让上层拿到,在底层的PCB代码中也是有这两个值的。exit_signal和exit_code。

子进程执行完毕后,代码和数据可以释放,但是PCB不能,因为其中包含退出信息,需要等父进程来接收。

waitpid成功了就返回接收的那个子进程的pid,失败了就返回-1,怎么才能接受失败呢?

搞一个限定接收某一个进程,但是给父进程传的id根本不是这个进程的,接收不到就失败了。

但是吧,exit的信息如果这么用位运算,代码不易读懂,所以系统提供了两个宏

  • WIFEXITED:若为正常终止子进程返回的状态,则为真(检查进程是否正常退出)
  • WEXITSTATUS:若WIFEXITED非零,提取子进程退出码(查看进程的退出码)
int main()
{pid_t id = fork();if(id < 0){perror("fork");return 1;}else if(id == 0){int cnt = 5;while(cnt){printf("i am child, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(0);}else{int cnt = 10;while(cnt){printf("i am father, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}pid_t ret = wait(NULL);int status;if(ret == id){if(WIFEXITED(status)){printf("进程是正常跑完的,退出码:%d\n", WEXITSTATUS(status));}else{printf("进程出异常了\n");}printf("wait success, ret:%d\n", ret);}else{printf("wait failed\n");}}return 0;
}

options:

        阻塞等待的意思就是,如果子进程状态不满足,父进程就一直等待,直到目标子进程状态改变(退出、暂停或被信号终止),父进程需要等待子进程完成后再继续执行

        非阻塞轮询就是使用 WNOHANG 选项waitpid(pid, &status, WNOHANG)立即返回

  • 如果目标子进程已退出/终止,则返回其 PID,并填充 status

  • 如果子进程仍在运行,则返回 0(不阻塞)。

#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>int main() 
{pid_t pid = fork();if (pid == 0) {// 子进程sleep(2);  // 模拟子进程运行printf("Child process exiting.\n");_exit(42);} else {// 父进程int status;while (1) {pid_t ret = waitpid(pid, &status, WNOHANG);  // 非阻塞轮询if (ret == pid) {printf("Child exited with code: %d\n", WEXITSTATUS(status));  // 输出 42break;} else if (ret == 0) {printf("Child still running...\n");sleep(1);  // 避免 CPU 忙等待} else {perror("waitpid error");break;}}}return 0;
}

父进程不会阻塞,而是每隔 1 秒检查一次子进程状态。 

 ret = 0就说明等待的条件没有就绪,非阻塞轮询的意义就是可以去做别的事情然后继续等待。

相关文章:

  • 初学者的AI智能体课程:构建AI智能体的十堂课
  • 在k8s中,如何实现服务的访问,k8s的ip是变化的,怎么保证能访问到我的服务
  • Perspective,数据可视化的超级引擎!
  • K8S常见问题汇总
  • PDF生成模块开发经验分享
  • [5-2] 对射式红外传感器计次旋转编码器计次 江协科技学习笔记(38个知识点)
  • XL32F001国产低成本单片机,24MHz主频,24KB Flash,3KB SRAM
  • 【探寻C++之旅】第十三章:红黑树
  • Python Cookbook-7.8 使用 Berkeley DB 数据库
  • TensorFlow 2.x入门实战:从零基础到图像分类项目
  • 物流无人机自动化装卸技术解析!
  • Python在自动驾驶实时数据处理中的应用:让AI驾驶更智能、更高效
  • Python奶茶系统
  • 能耗优化新引擎:EIOT平台助力企业降本增效
  • Redis实现分布式获取全局唯一自增ID的案例。
  • 二极管的动态特性
  • 如何设置内网映射端口到外网访问?哪些软件可以进行端口映射?
  • 多级路由器如何避免IP冲突
  • Go:简洁高效,构建现代应用的利器
  • VR博物馆,足不出户云逛展
  • 习近平同俄罗斯总统普京茶叙
  • 中华人民共和国和俄罗斯联邦关于进一步加强合作维护国际法权威的联合声明
  • 国家主席习近平同普京总统签署关于进一步深化中俄新时代全面战略协作伙伴关系的联合声明
  • 美联储主席:关税“远超预期”,美联储实现目标的进程或被推迟至明年
  • “用鲜血和生命凝结的深厚情谊”——习近平主席署名文章中的中俄友好故事
  • 上海一中院一审公开开庭审理被告人胡欣受贿案