⭐️这篇博客就要开始聊一聊进程控制相关的内容了,这部分的内容十分的丰富且十分的重要,学好这一块内容是非常有必要的
fork这个函数我在第一次讲进程创建的那篇博客中介绍过了,关于fork的返回值和用法可以去看右边这篇博客,这里就简单说明一下。(Linux进程)
fork函数也是一个系统调用接口,为当前进程创建子进程,子进程返回0,父进程返回子进程的pid,出错返回-1
进程调用fork函数,内核需要做什么?
fork之后执行什么?
父子进程共享一份代码,fork之后,一起执行fork之后的代码,且二者之间是独立的,不会相互影响
代码如下:
#include
#include
#include
int main()
{
pid_t ret = fork();
if (ret < 0)
{
perror("fork");
return 1;
}
else if (ret == 0)// 子进程
{
printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
else if (ret > 0)// 父进程
{
printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
sleep(1);
return 0;
}
通常情况下,父子进程共享一份代码,且数据也是共享的,当任意一方试图写入更改数据,那么这一份便要以写时拷贝的方式各自私有一份副本。
从图中可以看出,发生写时拷贝后,修改方将改变页表中对该份数据的映射关系,父子进程各自私有那一份数据,且权限由只读变成了只写。
思考下面几个问题:
代码是不可以被修改的,所以各自私有很浪费空间,大多数情况下是共享的,但要注意的是,代码在特殊情况下也是会发生写时拷贝的,也就是进程的程序替换(后面会单独介绍)。
a.可以减少空间的浪费,在双方都不对数据或代码进行修改的情况下,各自私有一根数据和代码是浪费空间的;
b.维护进程之间的独立性,虽然父子进程共享一份数据,但是父子中有一方对数据进行修改,那么久拷贝该份数据到给修改方,改变修改方中页表对这份数据的映射关系,然后对数据进行修改,这样不管哪一方对数据进行修改都不会影响另一方,这样就做到了独立性。
答案是否定的。如果没有修改的数据进行拷贝,那么这样还是会造成空间浪费的,没有被修改的数据还是可以共享的,我们只需要将修改的那份数据进行写时拷贝即可。
正常终止: 可以通过echo $?查看进程退出码,之前的博客中有介绍过
main函数退出的时候,return的返回值就是进程的退出码。0在函数的设计中,一般代表是正确而非0就是错误。
实例演示:
// 实例1
int main()
{
return 0;
}
// 实例2
int main()
{
return 0;
}
在任意位置调用,都会使得进程退出,调用之后会执行执行用户通过 atexit或on_exit定义的清理函数,还会 关闭所有打开的流,所有的缓存数据均被写入
实例演示:
int main()
{
cout << "12345";
sleep(3);
exit(0);// 退出进程前前会执行用户定义的清理函数,且刷新缓冲区
return 0;
}
int main()
{
cout << "12345";
sleep(3);
_exit(0);// 直接退出进程
return 0;
}
代码运行结果如下: 直接退出进程,不刷新缓冲区
异常终止:
进程等待的必要性:
wait的函数原型如下:
#include
#include pid_t wait(int*status); 函数返回值
返回值有两种,一种是等待进程的pid,另一种就是 -1,等待成功返回等待进程的pid,等待> 失败就返回-1
函数参数:
status是一个输出型参数,可以通过传地址获得进程退出状态,如果不想关心进程退出状态,就传 NULL
实例演示 让子进程先运行5s,然后退出进程,子进程由S状态变为Z状态,父进程等待子进程,回收子进程资源后,子进程变为Z状态变为X状态,10秒回父进程退出
代码如下:
#include
#include
#include
#include
#include
#include
int main()
{
pid_t ret= fork();
if (ret< 0){
cerr << "fork error" << endl;
}
else if (ret== 0){
// child
int count = 5;
while (count){
printf("child[%d]:I am running... count:%d\n", getpid(), count--);
sleep(1);
}
exit(1);
}
// parent
printf("father begins waiting...\n");
sleep(10);
pid_t id = wait(NULL);// 不关心子进程退出状态
printf("father finish waiting...\n");
if (id > 0){
printf("child success exited\n");
} else{
printf("child exit failed\n");
}
//父进程再活5秒
sleep(5);
return 0;
}
命令行监控脚本如下:
while :; do ps axj | head -1 && ps axj | grep test | grep -v grep ; sleep 1; echo "############"; done
代码运行结果如下:
子进程由S状态变为Z状态
父进程等待子进程,回收子进程资源后,子进程变为Z状态变为X状态
函数原型如下:
pid_ t waitpid(pid_t pid, int *status, int options);
函数返回值:
参数:
pid=-1时,可以等待任一个子进程,与wait等效
pid>0时,等待和pid相同的ID的子进程
是一个输出型参数,不想关心进程退出状态就传NULL
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID(可以进行基于阻塞等待的轮询访问)
0:阻塞等待(等待期间父进程不执行任何操作)
实例演示:
代码如下:
#include
#include
#include
#include
#include
#include
int main()
{
pid_t ret= fork();
if (ret< 0){
cerr << "fork error" << endl;
}
else if (ret== 0){
// child
int count = 5;
while (count){
printf("child[%d]:I am running... count:%d\n", getpid(), count--);
sleep(1);
}
exit(1);
}
// parent
printf("father begins waiting...\n");
sleep(10);
pid_t id = waitpid(-1, NULL, 0);// 不关心子进程退出状态,以阻塞方式等待
printf("father finish waiting...\n");
if (id > 0){
printf("child success exited\n");
} else{
printf("child exit failed\n");
}
//父进程再活5秒
sleep(5);
return 0;
}
status的几种状态:(我们只研究status的低16位)
看图可以知道,低7位代表的是终止信号,第8位时core dump标志,高八位是进程退出码(只有正常退出是这个退出码才有意义)
status的0-6位和8-15位有不同的意义。我们要先读取低7位的内容,如果是0,说明进程正常退出,那就获取高8位的内容,也就是进程退出码;如果不是0,那就说明进程是异常退出,此时不需要获取高八位的内容,此时的退出码是没有意义的。
实例演示:
#include
#include
#include
#include
#include
#include
int main()
{
pid_t ret = fork();
if (ret < 0){
cerr << "fork error" << endl;
}
else if (ret == 0){
// child
int count = 5;
while (count){
printf("child[%d]:I am running... count:%d\n", getpid(), count--);
sleep(1);
}
exit(1);
}
// parent
printf("father begins waiting...\n");
int status;
pid_t id = wait(&status);// 从status中获取子进程退出的状态信息
printf("father finish waiting...\n");
if (id > 0 && (status&0x7f) == 0){
// 正常退出
printf("child success exited, exit code is:%d\n", (status>>8)&0xff);
}
else if (id > 0){
// 异常退出
printf("child exit failed,core dump is:%d,exit singal is:%d\n", (status&(1<<7)), status&0x7f);
}
else{
printf("father wait failed\n");
}
if (id > 0){
printf("child success exited\n");
} else{
printf("child exit failed\n");
}
return 0;
}
操控者: 操作系统
阻塞的本质: 父进程从运行队列放入到了等待队列,也就是把父进程的PCB由R状态变成S状态,这段时间不可被CPU调度器调度
等待结束的本质: 父进程从等待队列放入到了运行队列,也就是把父进程的PCB由S状态变成R状态,可以由CPU调度器调度
#include
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id < 0){
cerr << "fork error" << endl;
}
else if (id == 0){
// child
int count = 5;
while (count){
printf("child[%d]:I am running... count:%d\n", getpid(), count--);
sleep(1);
}
exit(0);
}
// 阻塞等待
// parent
printf("father begins waiting...\n");
int status;
pid_t ret = waitpid(id, &status, 0);
printf("father finish waiting...\n");
if (id > 0 && WIFEXITED(status)){
// 正常退出
printf("child success exited, exit code is:%d\n", WEXITSTATUS(status));
}
else if (id > 0){
// 异常退出
printf("child exit failed,core dump is:%d,exit singal is:%d\n", (status&(1<<7)), status&0x7f);
}
else{
printf("father wait failed\n");
}
}
#include
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id < 0){
cerr << "fork error" << endl;
}
else if (id == 0){
// child
int count = 5;
while (count){
printf("child[%d]:I am running... count:%d\n", getpid(), count--);
sleep(1);
}
exit(0);
}
// 基于阻塞的轮询等待
// parent
while (1){
int status;
pid_t ret = waitpid(-1, &status, WNOHANG);
if (ret == 0){
// 子进程还未结束
printf("father is running...\n");
sleep(1);
}
else if (ret > 0){
// 子进程退出
if (WIFEXITED(status)){
// 正常退出
printf("child success exited, exit code is:%d\n", WEXITSTATUS(status));
}
else{
// 异常退出
printf("child exited error,exit singal is:%d", status&0x7f);
}
break;
}
else{
printf("wait child failed\n");
break;
}
}
}
fork创建子进程后一般会有两种行为:
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
思考几个问题:
把磁盘中的程序的代码和数据用加载器加载进特定的进程的上下文中,底层用到了exec系列的程序替换函数
答案是没有的。因为进程替换前后,没有创建新的PCB、虚拟内存和页表等数据结构,也就是进程的这些数据结构没有发生变化,进程替换只是对物理内存中的数据和代码进行了修改,前后进程的ID没有发生改变,所程序替换不创建新进程
由于进程替换会把新程序的代码和数据加载到特定的进程,为了让父子进程之间具有独立性,修改的代码和数据都要发生写时拷贝,这样才不会影响父进程的数据和代码
有六种以exec开头的函数,原型如下: 操作系统其实值提供了第六个系统调用接口,其他五个都是由第六个系统调用接口封装出来的
#include
extern char **environ;
int execl( const char *path, const char *arg, ...);// 可变参数列表,可以放入多个参数
int execlp( const char *file, const char *arg, ...);
int execle( const char *path, const char *arg , ..., char * const envp[]);
int execv( const char *path, char *const argv[]);
int execvp( const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
函数返回值: 调用出错返回-1,没有调用成功的的返回值
函数参数:
函数名解释:
函数的使用方法:
函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 列表 | 是 | 是 |
execlp | 列表 | 否 | 是 |
execle | 列表 | 是 | 否,自己组装环境变量 |
execv | 数组 | 是 | 是 |
execvp | 数组 | 否 | 是 |
execve | 数组 | 是 | 否,自己组装环境变量 |
函数调用案例如下:
int main()
{
// 自己组装的环境变量
char* myenv[] = {"MYENV=you can see my", NULL};
// 列表形式传参
execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
execp("ls", "ls", "-l", "-a", NULL);
exece("./mycmd", "mycmd", NULL, myenv);
// 数组形式传参
char* const argv[] = {"ls", "-l", "-a", NULL};
execv("/usr/bin/ls",argv);
execvp("ls", argv);
char* const agrv1[] = {"mycmd", NULL};
execve("./mycmd", agrv1, myenv);// 调用自己的程序
}
实例演示
实例1: 用当前路径下的mycmd程序替换自己的程序,使用execvp
代码如下:
/******************************mytest.cc******************************/
#include
#include
int main()
{
printf("I am a process:%d\n", getpid());
// int ret = execl("/usr/bin/ls", "ls", "-a", "-l", NULL); // 需要添加相对路径或者绝对路径
//int ret = execlp("ls", "ls", "-a", "-l" NULL);// p 自动搜索环境变量PATH
//int ret = execlp("ls", "ls", "-al", NULL);// p 自动搜索环境变量PATH
char* const arg[] = {
"ls",
"-a",
"-l",
NULL
};
//execv("/usr/bin/ls", arg);
//execvp("ls", arg);
char* const MY_ENV[] = {"myenv=you can see me", NULL};
execle("./mycmd", "mycmd", NULL, MY_ENV);
printf("you should run here...\n");
return 0;
}
/******************************mycmd.cc******************************/
int main()
{
printf("I am a process:%d\n", getpid());
printf("myenv:%s\n", getenv("myenv"));
return 0;
}
代码运行结果如下:
实例2: 子进程进行程序替换,父进程阻塞等待子进程退出状态,观察现象
#include
#include
#include
#include
#include
using namespace std;
int main()
{
pid_t id = fork();
if (id == 0){
// child
sleep(3);
execlp("ls", "ls", "-al", NULL);
exit(1);
}
else if (id < 0){
perror("fork error");
return 1;
}
// parent
pid_t ret = waitpid(id, NULL , 0);
if (ret > 0){
printf("cmd run done...\n");
}
return 0;
}
代码运行结果如下: 可见父子进程直接具有独立性,其中一个进程被替换,另一个进程不受影响
要写一个shell,需要循环以下过程:
#include
#include
#include
#include
#include
#include
#define SIZE 256
#define NUM 16
int main()
{
char buf[SIZE];// 命令行缓冲区
while (1){
// 清空缓冲区
buf[0] = '\0';
const char* cmd_line = "[temp@VM-0-9-centos MyShell]#";
printf("%s", cmd_line);
fgets(buf, SIZE, stdin);
buf[strlen(buf)-1] = '\0'; // 把buf最后一个字符'\n'置为'\0'
// strtok分割字符
char* argc[NUM];
argc[0] = strtok(buf, " ");
int i = 0;
for (i = 1; argc[i-1]; ++i){
argc[i] = strtok(NULL, " ");
}
pid_t id = fork();
if (id < 0){
// 进程创建失败
perror("fork error");
continue;
}
else if (id == 0){
// child
// 进程替换
execvp(argc[0], argc);
exit(1);
}
// parent
// 父进程通过阻塞等待方式读取子进程退出信息
int status;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0){
// 等待成功
if (WIFEXITED(status)){
// 子进程正常退出
printf("exit code is: %d\n", WEXITSTATUS(status));
}
else{
// 子进程异常退出
printf("exit failed, exit singal is %d\n", WIFEXITED(status));
}
}
else{
printf("wait failed\n");
}
}
return 0;
}
代码运行演示如下: 可以看出,该命令还解释器对基本的指令可以解释,但是管道,重定向都不行,后序的知识可以继续完善这个小程序
我们只是做一个小实验,所以这里我们选择创建一个新用户来完成该炒作
步骤: