进程控制
# 进程创建
# fork函数
**fork函数创建子进程:**fork从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
内核做的工作:
- 进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
执行下面的代码:
#include<iostream>
using namespace std;
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int global_val = 100;
int main() {
// 物理空间 or 虚拟空间
pid_t id = fork();
if (id == 0) {
// 子进程
global_val = 200;
cout << "子进程:" << "PID = " << getpid() << " PPID = " << getppid() << " global_val = " << global_val << endl;
} else if (id > 0) {
sleep(3);
cout << "父进程:" << "PID = " << getpid() << " PPID = " << getppid() << " global_val = " << global_val << endl;;
} else {
cout << "fork error" << endl;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
输出结果:
子进程:PID = 3111169 PPID = 3111168 global_val = 200
父进程:PID = 3111168 PPID = 3096584 global_val = 100
2
会发现父进程在打印全局变量的时候,并没有被子进程修改,为什么呢?
写时拷贝 通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,这样可以高效的使用内存空间
# 返回值
- 如果子进程创建成功,在父进程中返回子进程的PID,而在子进程中返回0
- 如果子进程创建失败,则在父进程中返回 -1
# 用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个全新的,不同的程序。例如子进程从fork返回后,调用exec函数
# 进程终止
进程终止有三种情况:代码运行完毕,结果正确(return 0)、代码运行完毕,结果不正确(return !0)和代码异常终止。
# 退出码
以0表示代码执行成功,以非0表示代码执行错误
- main函数 return 退出码 = exit(退出码)
- exit(退出码):exit函数可以在代码中的任何地方退出进程,并且会自动刷新缓冲区,这点与_exit不同(这里的缓冲区在用户级的缓冲区)
# 进程等待
子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏。所以,父进程需要通过进程等待的方式,回收子进程资源,获取子进程的退出信息。
# 进程等待的函数
#include <sys/wait.h>
#include <sys/types.h>
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
2
3
4
5
wait
函数表示等待任意子进程,参数表示子进程的状态,不关心可以设置为NULL,如果等待成功返回子进程的PID,否则返回-1
waitpid
函数表示等待指定或者任意子进程
- 参数:
- pid:待等待子进程的pid,若设置为-1,则等待任意子进程,等价于wait
- status:输出型参数,获取子进程的退出状态,不关心可设置为NULL。
- options:当设置为WNOHANG时,若等待的子进程没有结束,则waitpid函数直接返回0,不予以等待。若正常结束,则返回该子进程的pid。
- 返回值
- 等待成功返回被等待进程的pid。
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0。
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
status:
status是一个输出型参数,由操作系统进行填充。如果对status参数传入NULL,表示不关心子进程的退出状态信息。否则,操作系统会通过该参数,将子进程的退出信息反馈给父进程。
在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。
exitCode = (status >> 8) & 0xFF; // 退出码
exitSignal = status & 0x7F; // 退出信号
2
# 阻塞 vs 非阻塞
当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情,这种等待叫做阻塞等待。我们可以让父进程不要一直等待子进程退出,而是当子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息,即非阻塞等待。
#include<iostream>
using namespace std;
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t id = fork();
if (id == 0) {
// child
int cnt = 5;
while (cnt) {
cout << "子进程:" << getpid() << "父进程:" << getppid() << endl;
cnt--;
sleep(1);
}
exit(12);
} else if (id > 0) {
// parent: wait child
int status = 0;
// int ret = waitpid(id, &status,WNOHANG); // 非阻塞等待
// cout << "exitcode: " << ((status >> 8) & 0xFF) << " " << "exitsig: " << (status & 0x7F) << endl;
while (true) { // 轮询
int ret = waitpid(id, &status, WNOHANG); // 非阻塞等待
if (ret == 0) {
cout << "pid指定的子进程没有退出!" <<
endl;
} else if (ret > 0) {
// 返回子进程的pid了 ret == id
cout << "等待成功,子进程退出!" << endl;
cout << "exitcode: " << ((status >> 8) & 0xFF) << " " << "exitsig: " << (status & 0x7F) << endl;
cout << "ret: " << ret << endl;
break;
} else {
// waitpid调用失败
cout << "waitpid call fail!" << endl;
break;
}
sleep(1);
}
} else {
cout << "fork error!" << endl;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 进程程序替换
fork创建的子进程可以执行和父进程相同的程序,也可以执行一段全新的程序。这个过程就是进程程序替换。
# 替换函数
使用exec
函数:
#include <unistd.h>
extern char **environ;
int execl(const char *pathname, const char *arg, .../*, (char *) NULL */);
int execlp(const char *file, const char *arg, .../*, (char *) NULL */);
int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
2
3
4
5
6
7
8
9
10
int execl(const char *pathname, const char *arg, .../*, (char *) NULL */);
第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。
int execlp(const char *file, const char *arg, .../*, (char *) NULL */);
第一个参数是要执行程序的名字,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾
int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);
第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾,第三个参数是你自己设置的环境变量。
int execv(const char *pathname, char *const argv[]);
第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
int execvp(const char *file, char *const argv[]);
第一个参数是要执行程序的名字,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
int execvpe(const char *file, char *const argv[], char *const envp[]);
第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾,第三个参数是你自己设置的环境变量。
例如:
execl("/usr/bin/ls","ls","-a","-l",NULL); // 执行ls
# 替换原理
用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),若想让子进程执行另一个程序,往往需要调用一种exec函数。
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行。