进程的控制
目录
一.进程创建
1.fork初始

fork创建的子进程之后进行fork()后面的代码,不会执行之前的代码
2.写时拷贝
关于fork,我们之前了解过,在创建子进程时若子进程进行w行为系统会对此开一段空间并将要更改的数据拷贝一份让子进程进行更改,这是我们对写时拷贝的了解
下面来扩展一下,在我们在跑我们的代码(进程)时,进程中有两部分,数据段和代码段,操作系统怎么知道你访问的字段是数据段还是代码段?是通过虚拟地址每个区间来判断的;但他们是只有只读权限并没有w(更改)权限,当我们的子进程要更改数据时,操作系统会判断它是子进程并要更改数据,然后操作系统更改页表的权限使操作系统实现写时拷贝

写时拷贝的作用:
1.保证进程的独立性
2.减少子进程创建的时间
3.节省内存空间
3.fork的常规用法
1.⼀个⽗进程希望复制⾃⼰,使⽗⼦进程同时执⾏不同的代码段。例如,⽗进程等待客户端请求,
⽣成⼦进程来处理请求
2.⼀个进程要执行⼀个不同的程序。例如⼦进程从fork返回后,调用exec函数。
4.fork调用失败原因
1.系统中存在大量进程,内存空间不足
2.用户创建的进程数超过了系统限制
二.进程终止
进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码,一般进程终止主要是针对父进程拿到子进程的退出码来判断子进程是否正常退出
1.进程退出的场景
进程退出无非以下三种情况:

引入:
在我们写main函数时,一般都会返回0,main函数的返回值返回给谁了呢?那么为什么要返回0呢?这个0有没有特殊含义?返回其他值又说明什么?
main函数返回值表示你程序的执行情况,而这些值称之为退出码,返回0表示程序正常终止且无错误,非零值的含义:非零返回值(通常为1-255)表示程序异常终止,每一个异常值对应一个错误类型(比如数组越界,打开的文件不存在等),main函数结束表示一个进程结束,其他函数则表示自己的函数调用完成,返回对应返回值
2.常见进程的退出方法

echo $?:打印最近一个程序(进程)的退出码
进程的退出码是要写到数据内部(PCB)的,若进程异常终止,那么退出码将无意义,对于正常退出和异常终止,两者有什么区别呢?
正常退出:进程主动终止,一般是可控错误处理
异常终止:进程被动终止,一般异常终止是进程收到了系统的信号,如崩溃、被杀死(9号信号)、资源耗尽等
2.1.正常终止

其中status表示进程的退出码(退出码对应一个字符串(错误信息);当然也可以自己定义(自己写一个exit的函数))
关于exit和_exit的区别

exit是C语言的库函数,_exit是系统调用;我们之前提到过,库函数是对系统调用的一个包装
区别是exit退出的时候,会对缓冲区进行刷新,_exit退出的时候,不会对缓冲区进行刷新
那么缓冲区在哪里呢?对比上面两个函数我们可以推出
缓冲区一定不是系统内部的缓冲区,是C语言提供的库缓冲区
我们在exit(3)和_exit(3)时,都会将进程的退出码改为3
2.2.异常退出
进程由信号、段错误等外部事件触发被动退出,此时父进程通过 WIFSIGNALED(status) 宏判断并获取子进程的异常值

三.进程等待
1.进程等待的必要性

2.进程等待的方法
为什么要进程等待?
1.回收子进程资源(必要的)2.获取子进程退出信息(次要的)
2.1.wait
wait:等待任意一个退出的子进程,用于回收子进程的僵尸状态,status为输出型参数

代码展示

由于我们只有一个子进程,所以我们wait只会等待这个子进程结束后并回收成功返回该进程的pid(>0)
此时我们分为3个阶段,前5秒子进程和父进程同时处于运行(S+)状态,第五秒时,子进程结束,处于僵尸状态等待父进程回收,此时子进程处于僵尸状态,父进程处于运行状态,第十秒时,父进程调用wait函数回收了子进程,子进程结束,只有一个父进程处于运行状态,又等待了十秒后,父进程结束
2.2.waitpid
与wait相同,可指定等待一个进程。options表示阻塞控制,等待一个进程可以阻塞等待同样也可以非阻塞等待(后面讲)


将部分代码改成这样和上述的wait的作用一样,只不过是通过waitpid实现

2.3.获取子进程的status
什么是输出型参数呢?
一般通过形参(指针,引用)把值带出来的,我们称之为输出型参数
我们父进程是通过获取子进程的退出码来判断子进程是否正常运行完毕,又或者异常退出

下面我们编写一个函数使他的退出码设置为1,但为什么打印出来为256呢?

我们意识到,status不止保存退出码,还有其他东西

status是类似于位图一样,共32个bite位,高16位我们不用也不考虑,低16位中0-7位表示异常终止信号,第8位位core dump值(后面信号讲),第7-15位表示退出码,所以我们在打印status值时,若程序正常退出,我们需要将status右移8位来获取退出码,此时退出码就成为了1

在没有异常的情况下,status低7位都为0,此时获取退出码我们可以通过WEXITSTATUS(status)来获取,异常值我们可以通过status&0x7F(获取低7位有效值)来获取
如果一个程序异常退出,那么获取他的退出码就没有意义了,这时我们就要看异常值
父进程是怎么做到收到子进程的退出信息的?

子进程处于僵尸状态时,他的PCB中存在exit_code和exit_signal两个整型变量来存放他的退出码和退出信号,当子进程的main函数退出时会将退出码保存到exit_code中,退出信号看有没有异常退出,没有就是0;此时父进程通过系统调用waitpid(&status)来获取子进程的退出信息。
我们理解了,系统管理进程,若进程想要获取另一个进程的信息(PCB),需要通过系统调用帮忙
2.4.阻塞等待和非阻塞等待
阻塞等待:当一个进程(或线程)调用某个操作(如 I/O 操作、进程同步操作等)时,如果该操作不能立即完成,调用者会被挂起(进入阻塞状态),直到操作完成或超时后才能继续执行。
非阻塞等待:进程/线程在等待期间可以继续执行其他任务。(可以让等待方完成自己的任务)
非阻塞轮询:每隔一段时间就会检查阻塞程序是否完成,直到阻塞程序操作完成
四.进程的程序替换
顾名思义,就是利用代码来实现指令,我们通常使用exec系列代码来实现程序替换
1.替换原理
代码段指的是我们编写的这块代码,数据段就是内部定义的各种变量,进程一旦调用execl,他就会把我们新指定路径下新的程序覆盖到原代码段和数据段中,execl并不创建新进程,所以调用execl前后进程id不变

我们通过execl函数来实现程序替换,若成功则后续程序替换,并不会执行后续代码,失败返回-1,并执行后续代码,故exec系列函数,我们不对返回值做判断,只要返回就是失败!


我们想要执行后面的代码怎么办呢?我们可以通过子进程来实现程序替换,父进程来实现后续代码
2.替换函数
其实exec开头的函数有6种,称之为exec系列函数

path为路径名;arg为命令行执行命令时你怎么写,你就怎么传(多参数),但结尾必须以NULL结尾;file为要执行的文件名(命令名,如ls);argv[]为提供一个命令行参数表(就是一个指针数组),执行表中的命令;envp[]为环境变量表

exec后字母的含义:

而我们的exec系列函数不止能替换系统调用还可以替换我们写的代码(包括C++,Python等)
![]()
关于execvpe

若我们这样写,我们传入的新环境变量会将旧的环境变量替换掉,若想新增环境变量,我们需要使用putenv函数(stdlib.h),若要用execvpe,则要先用putenv(若环境变量表中存在该变量为替换,不存在为新增)将要新增的变量加入到环境变量表中,再传environ获取全部的环境变量

在我们查询exec系列函数时,发现下图这几个都属于EXEC第三章中

而execve再手册第二章中

说明execve为系统调用,而上述那些方法只是C语言对它的封装
五.myshell的实现
要写一份自己的shell,需要循环以下功能

代码实现:
首先我们需要创建自己的myshell.cc的c++文件,包含头文件

在此之前我们需要指定创建数组的大小,格式;以及各个全局变量(命令行参数表)

并在主函数中循环这几个功能

1.输出命令行提示符

我们观察我们使用的xshell,需要我们的用户名,当前主机名称,当前路径等信息,这些信息环境变量表中有,我们可以通过getenv函数来获取这些变量信息
![]()

获取完我们想要的信息后,接下来就是打印出来,我们创建完prompt数组后通过MakeCommandLine()方法来存放我们指定命令行格式的字符串,大小为1024;并输出它(到\0之前),为了保险起见再通过fflush(stdout)刷新缓冲区
int snprintf(char *str, size_t size, const char *format, ...) 设将可变参数(...)按照 format 格式化成字符串,并将字符串复制到 str 中,size 为要写入的字符的最大数目,超过 size 会被截断,最多写入 size-1 个字符。
由于我们只输出pwd中最后一个路径,所以我们通过rfind倒着找"/",找到后返回/后的子串



2.获取用户输入的命令
我们需要获取用户在输入指令时的字符串,此时我们需要创建字符数组来接收这个字符串,接收失败则进if跳过此次循环

我们用scanf接收字符串的话会将空格识别为分隔符,只会拾取空格之前的字符,故我们要通过fgets()函数来解决有分隔符的问题,用strlen是获取\0之前的元素,再-1是为了清理掉\n,此时我们获取到了用户输入的命令
C 库函数 char *fgets(char *str, int n, FILE *stream) 从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。

此时我们判断如果接收到用户输入的指令就继续向下执行,没有接收到(用户就摁了一个回车)就continue跳过这次循环,进行下次循环
3.命令行分析
我们在获取用户输入的命令之后,需要将这个字符串拆分为指令+选项的子串格式,分析失败进if跳过此次循环

我们将空格作为分隔符,并依次获取子串存放到我们创建的命令行参数表中(g_argv[]),关于strtok()函数,我们刚开始需要传我们获取用户输入命令的字符串,它会在第一个空格停下来,若要从第一个空格处继续并持续此过程,我们接下来要传nullptr,和分隔符,这样它就会一直将这些子串传到我们的命令行参数表中
C 库函数 char *strtok(char *str, const char *delim) 分解字符串 str 为一组字符串,delim 为分隔符。

我们可以通过打印我们的命令行参数表来判断我们是否正确的传入

此时我们判断如果命令行参数表中元素个数不为空就继续向下执行,如果为空就continue跳过这次循环,进行下次循环
4.执行命令
接下来就用到我们现在学到的知识,我们创建子进程,让子进程来帮我们实现系统调用;由于我们有命令行参数表,所以我们使用execvp函数即可来实现命令的程序替换,父进程再通过waitpid回收子进程的PCB

我们需要接收退出码,所以要定义一个退出码lastcode的全局变量并返回用于echo $?


此时我们遇到了一个问题,在我们使用cd ..等指令时,子进程更改的是子进程的路径,我们知道进程具有独立性,所以在我们用pwd打印时会发现路径没有改变,此时关于一些内核指令,我们要单独实现
5.检测并处理內键命令
在执行命令前我们需要先判断是否为內键命令,再执行命令,故要和执行命令交换一下位置,为內键命令进if来实现内键命令,就不执行执行命令的代码了

5.1.cd
关于內键命令首先我们会想到cd命令,此时我们若想影响到父进程的路径,就需要用到系统调用chdir()函数,它用于更改当前进程的工作目录,如果我们只执行cd(通过g_argc判断)的话,默认回到家目录,所以我们此时要获取家目录



此时我们使用cd命令会发现虽然当前路径变了但是pwd的路径没有变

我们知道进程更改路径时,进程先变,环境变量表再通过系统调用后变,所以我们需要重写我们的Getpwd函数,此时我们先要定义两个全局变量表,一个用来存放当前的工作路径,另一个存放当前工作路径的PWD=当前工作路径的格式(用于替换旧环境变量表中的PWD),我们使用getcwd系统调用来获取当前工作目录的绝对路径,接着通过putenv系统调用来替换PWD


此时我们通过cd更改当前路径的话,对应的环境变量表上也更改了

5.2.echo
此时我们仅需要再填一个判断条件即可,后续如果再有内键命令就再加判断

关于echo,我们常用echo=@?来获取最近一次进程的退出码,和echo $环境变量来获取环境变量的值
所以在我们使用完echo @?时输出退出码并需要注意吧退出码更新为0
使用echo $环境变量时,我们使用substr(1)来存放string字符串下标为1开始到结尾的环境变量名,接着通过getenv来获取环境变量的内容,最后如果获取成功则输出对应内容,获取内容为空或获取失败则直接返回

6.补充
shell在启动时,会获取系统的环境变量表,本来是从系统的配置文件中来,但他是由脚本实现,我们目前实现不了,所以我们需要从父shell获取即可

我们通过声明environ来获取环境变量表,并通过malloc 在堆内存(g_env[i])里开辟一块新的、可读写的内存区域,大小刚好能装下environ[i] 这个字符串,再让strcpy将内容复制过去


这样我们启动我们的myshell时;就有了属于我们自己的环境变量表啦

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)