Linux——进程控制

embedded/2024/9/24 13:24:05/

Linux——进程控制


文章目录

  • Linux——进程控制
  • 一、进程创建
  • 二、进程终止
    • 2.1 退出码
    • 2.2 进程退出场景
    • 2.3 进程常见退出方法
  • 三、进程等待
    • 3.1 进程等待的必要性
    • 3.2 进程等待的方法
  • 四、进程替换
    • 4.1 替换原理
    • 4.2 替换函数
    • 4.3 替换细节
  • 五、实现简易版myshell


一、进程创建

上篇文章中我们已经了解到,创建一个进程可以调用fork()函数

#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1

fork调用失败的原因

1. 系统中有太多的进程
2. 实际用户的进程数超过了限制

进程调用fork,内核会做以下操作:

1. 分配新的内存块和内核数据结构给子进程
2. 将父进程部分数据结构内容拷贝至子进程
3. 添加子进程到系统进程列表当中
4. fork返回,开始调度器调度

也就是父进程调用fork函数,操作系统会将父进程的代码和数据共享给子进程,并且按照父进程的PCB拷贝一份给子进程(特殊值,例如PID子进程还是自己的)

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,就会以写时拷贝的方式各自一份副本,如下图所示

在这里插入图片描述

fork常规用法

1. 一个父进程希望复制自己,通过fork的返回值进行分流,使父子进程同时执行不同的代码段
例如:父进程等待客户端请求,生成子进程来处理请求

2. 一个进程要执行一个不同的程序
例如:子进程从fork返回后,调用exec函数

关于fork的第一种用法,fork如何创建子进程,写实拷贝等概念,上篇文章进程概念中详解了
点击此处传送门:进程概念中篇
此篇文章主要讲述第二种方法 也就是 进程替换

二、进程终止

2.1 退出码

我们在平时写main函数的时候,总会写上return 0;为什么

main函数其实也是函数,是函数就会被调用执行,而如何被执行呢?
现在我们已经很清楚了,将其代码和数据加载到内存,并且建立对应的PCB,也就是创建一个进程

而子进程必然是父进程出来的,父进程为什么要创建子进程呢?
父进程创建子进程必然是为了让子进程执行某些任务

既然父进程创建子进程是为了让子进程执行某些任务,那么父进程肯定需要知道任务完成的怎么样,如何知道呢?
每个进程结束的时候都会返回退出码,退出码用来显示子进程的运行结果

main函数中的return 0;0 就是退出码
main函数的退出码是可以被父进程获取的,用来判断子进程的运行结果

退出码的作用

标定进程结束时程序是否正确

获取最近一个执行完毕进程的退出码:

echo $?

我们用0表示成功,非0表示失败。而非0的不同数字表示不同的错误

C语言的错误码 errno

错误码通常是衡量一个库函数或者是一个系统调用人个函数的调用情况
退出码通常是一个进程退出的时候,他的退出结果

strerror可以通过标准错误的标号,获得错误的描述字符串 ,将单纯的错误标号转为字符串描述,方便用户查找错误

我们将其中的内容打印出来
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.2 进程退出场景

进程退出场景一般有三种
在这里插入图片描述
进程出异常,本质是进程收到了对应的信号,自己终止了
所以,一个进程是否出异常,我们只要看有没有收到信号即可

查看进程退出信号
kill -l
在这里插入图片描述

总结:

进程退出分为两种:
1. 代码跑完进程正常退出
2. 代码没跑完异常退出

正常退出可以查看进程退出码,异常退出时退出码无意义,需要看信号
所以进程退出只需要看两个数字——退出码与信号

2.3 进程常见退出方法

正常终止(也就是上图中的代码跑完了,可以通过 echo $? 查看进程退出码):

1. 从main返回
2. 调用exit()
3. _exit()

异常退出

ctrl + c,信号终止

exit和_exit的区别

1. exit是库函数,_exit是系统调用,exit()函数本质上是封装了系统接口_exit()
2. exit终止进程的时候,会自动刷新缓冲区,_exit终止进程的时候,不会自动刷新缓冲区

在这里插入图片描述
return退出

return是一种更常见的退出进程方法,执行return n;等同于执行 exit(n);
因为调用main的运行函数会将main的返回值当做exit的参数

三、进程等待

3.1 进程等待的必要性

1. 子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏
2. 进程一旦变成僵尸状态,kill -9 都结束不了进程,因为谁也没有办法杀死一个已经死去的进程
3. 父进程需要知道派给子进程任务执行的结果,子进程是否正常退出,如果正常退出,结果对还是不对
4. 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

我们创建子进程是为了让它帮助父进程做某些事,所以父进程需要它的执行结果,所以在子进程退出的时候,会变成僵尸进程把退出结果写入PCB中,父进程来回收以便获得结果(退出状态),但是我们在前面讲过父子进程执行的先后顺序并不确定,所以有可能父进程先退出,造成内存泄漏,所以我们需要让父进程进行进程等待

什么是进程等待?
通过wait/waitpid的方式,让父进程(一般)对子进程进行资源回收的等待过程

为什么要进行等待?
解决子进程僵尸问题带来的内存泄漏问题------目前必须

父进程为什么要创建子进程?要让子进程来完成任务,子进程将任务完成的如何,父进程要不要知道?
要知道,需要通过进程等待的方式,获取子进程退出的信息(两个数字),不是必须的,但是系统需要提供这样的基础功能

3.2 进程等待的方法

在这里插入图片描述
wait方法

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);

返回值: 成功返回被等待进程pid,失败返回-1
参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

wait时进程等待能回收子进程僵尸状态,Z->X
如果子进程根本就没有退出,父进程必须在wait上进行阻塞等待直到子进程僵尸,wait自动回收,然后返回

关于阻塞等待 :一般而言,谁先运行不知道,但是最后一般都是父进程最后退出

waitpid方法

pid_t waitpid(pid_t pid, int *status, int options);

返回值
rid > 0,等待成功
rid == 0,等待是成功的,但是子进程还没有退出
rid < 0,等待失败
参数
pid
pid=-1,等待任一个子进程,与wait等效
pid>0,等待其进程ID与pid相等的子进程
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码(查看进程的退出码)
options:
0——阻塞等待(也就是父进程一直在询问子进程是否执行完毕)
WNOHANG——非阻塞等待(若pid指定的子进程没有结束,则waitpid()函数返回0,父进程执行自己的代码,做自己的事,过一段时间再询问子进程,也就是非阻塞轮询等待,若正常结束,则返回该子进程的ID)

获取子进程status

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
如果传递NULL,表示不关心子进程的退出状态信息
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)

在这里插入图片描述
根据上图中status结构的分析,我们可以通过对status进行逻辑操作和位运算操作拿到退出码和信号

printf("exit signal:%d,  exit code:%d \n", (status & 0x7f), (status >> 8 & 0xff)); 

Linux给我们提供了两个宏函数,更加方便的让我们知道程序是否正常退出,并且拿到正常退出时的退出码

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码(查看进程的退出码)

注意
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞
如果不存在该子进程,则立即出错返回

父进程如何得知子进程的退出信息呢? 通过wait/waitpid?

子进程退出的时候,要修改状态Z,并将子进程的退出信号和退出码写入自己的PCB中
此时父进程在调用wait/waitpid等待子进程,OS就会执行*statusp = (exit_code<<8) | exit_siganl;
将PCB中的退出码和信号写入变量status中,通过输出型参数返回给父进程

我们为什么不用全局变量获取子进程的退出信息? 而用系统调用?

进程具有独立性,父进程无法直接获得子进程的退出信息

四、进程替换

这里讲述的就是fork的第二种用法:一个进程要执行一个与父进程不同的程序

4.1 替换原理

用fork创建子进程后执行的是和父进程相同的程序(可以通过fork返回值进行判断分流,执行不同的代码分支)

如果我们想让子进程执行新的程序呢? 执行全新的代码和访问全新的数据,不再和父进程有瓜葛

程序替换

原理

子进程调用一种exec函数以执行另一个程序
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,执行的是新进程的代码和数据
调用exec并不创建新进程,所以调用exec前后该进程的id并未改变

在这里插入图片描述
由于是替换原来进程的代码和数据,如果替换成功,那么在原来进程后面的代码就不会执行,只会执行新进程的代码和数据

子进程怎么知道要从新的程序的最开始执行? 它怎么知道最开始的地方在哪里?

程序计数器,pc. eip
替换的代码和数据中有新程序的可执行入口地址,将他们加载到寄存器中

4.2 替换函数

在这里插入图片描述

在这里插入图片描述

如上图,有六种以exec开头的函数,统称exec函数,前五个是三号手册,最后一个是二号手册
所以前五个都是库函数,而第六个是系统接口,前五个本质上都是第六个封装而来
为了使用者有更好的体验,满足更多的调用场景,所以设计了多个适用调用接口

这些函数原型看起来很容易混,但有一定的规律

l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量

在这里插入图片描述

这些函数如果调用成功则加载新的程序从启动代码开始执行,并且不再返回
如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值

这里我们调用第一个函数execl来验证一下程序替换

在这里插入图片描述
执行程序
在这里插入图片描述

我们可以很明显的看到在执行程序替换后,原来程序下面的printf并没有执行,只执行了替换之前的printf
execl就是程序替换函数,execl执行完后,代码全部被覆盖,开始执行新程序的代码了,所以最后一个printf无法执行

4.3 替换细节

当我们进行程序替换的时候,子进程对应的环境变量,是可以直接从父进程来的,如何验证呢?

进程的创建方式,就决定了一定拥有父子进程之间的关系,父进程创建子进程,子进程会与父进程共享代码和数据,子进程的PCB会按照父进程的进行拷贝,其中就有父进程的环境变量

环境变量被子进程继承下去是一种默认行为,不受程序替换的影响,为什么?

通过地址空间可以让子进程继承父进程的环境变量数据
程序替换只替换新程序的代码和数据环境变量不会被替换

在这里插入图片描述

如果不传环境变量,父进程的环境变量会原封不动传递给子进程,可以直接用,但也可以手动将父进程环境变量传给子进程

char** environ是一个默认的全局的指针,指向环境变量表char *argv[]

同样在进程覆盖的时候,如果我们不调用有环境变量的接口,子进程的环境变量和父进程相同,替换不会改变环境变量
如果我们想传递我们自己的环境变量,我们可以直接构造环境变量表,调用接口,给子进程传递(覆盖传递)
在这里插入图片描述

如果我想新增传递呢?

int putenv(char *string);

可以把自定义环境变量导入到调用进程的环境变量中,这样先在父进程中添加环境变量,再传给子进程,就能做到新增传递了

五、实现简易版myshell

此处细节不做详细解释

主要分为四步:

1. 打印提示符并且获取用户命令字符串
2. 分割命令字符串
3. 判断是否是内建命令,如果是则执行(内建命令本质是shell内部的函数,用子进程的替换进程的方式无法完成任务,需要shell自己执行)
4. fork创建子程序,进行程序替换,执行对应的命令

代码如下
注意:由于是简易版myshell,加上个人能力有限,可能存在BUG

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>#define MAXCMD 1024
#define MAXARGC 64int lastcode = 0;
char pwd[1024];char* enval[1024];
int encount = 0;const char* getUser()
{const char* user = getenv("USER");if(user){return user;}else{return "none";}
}const char* getHOSTNAME()
{const char* name = getenv("HOSTNAME");if(name){return name;}else{return "none";}
}
const char* getPwd()
{const char* pwd = getenv("PWD");if(pwd){return pwd;}else{return "none";}
}int getUsercommand(char* command, int num)
{printf("[%s@%s %s]# ",getUser(),getHOSTNAME(),getPwd());char* s = fgets(command,num,stdin);if(s == NULL){return -1;}else{command[strlen(command)-1] = '\0';return strlen(command);}
}void commandSplit(char* in, char* out[])
{int cnt = 0;out[cnt++] = strtok(in," ");while(out[cnt++] = strtok(NULL," "));// for(int i = 0; out[i]; i++)// {//     printf("%s ",out[i]);// }}int exeCute(char* argv[])
{int n = fork();if(n < 0){return -1;}else if(n == 0){execvp(argv[0],argv);exit(1);}else{int status;pid_t pid =  wait(&status);if(pid > 0){lastcode = WEXITSTATUS(status);}return 0;}
}void cd(const char* path)
{chdir(path);char tmp[1024];getcwd(tmp,sizeof(tmp));sprintf(pwd,"PWD=%s",tmp);putenv(pwd);
}int builtCommand(char* argv[])
{if(strcmp(argv[0],"cd") == 0){char* path = NULL;if(argv[1] == NULL){path = NULL;}else{path = argv[1];}cd(path);return 0;}else if(strcmp(argv[0],"export") == 0){if(argv[1] == NULL){return 0;}strcpy(enval[encount],argv[1]);putenv(enval[encount++]);}else if(strcmp(argv[0],"echo") == 0){if(argv[1] == NULL){return 0;}if(*argv[1] == '$'){if(strcmp(argv[1]+1,"?") == 0){printf("%d\n",lastcode);lastcode = 0;}else{printf("%s\n",getenv(argv[1]+1));}return 0;}       else{for(int i = 1;argv[i];i++){printf("%s ",argv[i]);}printf("\n");return 0;}}
}int main()
{while(1){char command[MAXCMD];char* argv[MAXARGC];int n = getUsercommand(command,sizeof(command));if(n <= 0){continue;}commandSplit(command,argv);n = builtCommand(argv);if(n == 0){continue;}exeCute(argv);}return 0;
}

http://www.ppmy.cn/embedded/24570.html

相关文章

css实现dom脱离文档流定位固定位置

在CSS中&#xff0c;要使一个DOM元素脱离正常的文档流并定位在页面的右顶部&#xff0c;你可以使用position属性。通过设置position: absolute;或position: fixed;&#xff0c;可以将元素固定在视口或其最近的相对定位&#xff08;position: relative;&#xff09;的祖先元素中…

数据结构-堆

堆通常是一个可以被看做一棵树的数组对象。堆的具体实现一般不通过指针域,而是通过构建一个一维数组与二叉树的父子结点进行对应,因此堆总是一颗完全二叉树。对于任意一个父节点的序号n来说(这里n从0算),它的子节点的序号一定是2n+1,2n+2,因此可以直接用数组来表示一个堆…

新手向:HTML进阶

一&#xff0c;列表 列表分为有序列表&#xff0c;无序列表&#xff0c;定义列表三种 1.有序列表 ol 嵌套 li&#xff0c;ol 是有序列表&#xff0c;li 是列表条目 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8">…

数据结构--线性表之顺序表

本篇主要整理介绍数据结构--线性表的使用&#xff0c;持续更新中。 老铁们&#xff0c;整理不易&#xff0c;创作不易&#xff0c;先赞后看养成习惯&#xff0c;你的支持是对我更新最大的鼓励&#xff01; 线性表 知识框架 线性表概念&#xff1a;线性表 &#xff08; linear…

JavaEE技术之MySql高级(索引、索引优化、sql实战、View视图、Mysql日志和锁、多版本并发控制)

文章目录 1. MySQL简介2. MySQL安装2.1 MySQL8新特性2.2 安装MySQL2.2.1 在docker中创建并启动MySQL容器&#xff1a;2.2.2 修改mysql密码2.2.3 重启mysql容器2.2.4 常见问题解决 2.3 字符集问题2.4 远程访问MySQL(用户与权限管理)2.4.0 远程连接问题1、防火墙2、账号不支持远程…

设计模式- 访问者模式(Visitor Pattern)结构|原理|优缺点|场景|示例

设计模式&#xff08;分类&#xff09; 设计模式&#xff08;六大原则&#xff09; 创建型&#xff08;5种&#xff09; 工厂方法 抽象工厂模式 单例模式 建造者模式 原型模式 结构型&#xff08;7种&#xff09; 适配器…

centos学习-压缩和解压缩命令

CentOS 压缩与解压缩命令详解 在CentOS操作系统中&#xff0c;压缩和解压缩命令是极为常用的工具&#xff0c;用于对文件进行打包、压缩和解压缩操作。这些命令能够方便地处理大量的文件&#xff0c;使其更易于拷贝、移动和存储。本文将详细介绍CentOS中常见的压缩解压缩命令的…

情感类ppt素材

小清新手绘插画风毕业季毕业相册同学录画册纪念册PPT下载 - 觅知网这是一张关于清新毕业相册的PPT模板&#xff0c;清新风格设计&#xff0c;加上风为装饰元素&#xff0c;包含毕业相册、毕业季、毕业、同学、纪念等主题内容&#xff0c;也可用作毕业相册PPT、毕业季PPT、毕业P…