【打造你自己的Shell:编写定制化命令行体验】(一):https://developer.aliyun.com/article/1425812
3.2.1.wait方法
#include<sys/types.h> #include<sys/wait.h> pid_t wait(int*status); 功能: 默认会进行阻塞等待,如果子进程不退出,wait会一直等待 直到子进程退出,wait才会返回 wait会等待父进程名下所有的子进程 返回值: 成功返回被等待进程pid,失败返回-1。 参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
首先我们来验证一下wait确实能帮我们回收僵尸进程。
这个代码的含义是:这个程序会运行时间13秒,前5秒父子进程都在运行,再5秒,子进程处于僵尸状态,而父进程依然正常运行,随后父进程就开始回收,回收之后我们就可以看到僵尸状态不存在了,然后再过3秒,父进程就会退出。
fork之后,父子进程谁先运行我们是不确定的,这是由调度器决定,但是我们能确定谁先退出,父进程!
3.2.2.waitpid方法
我们创建一个子进程的目的是为了帮我们完成某些任务,所以我们就需要看子进程完成的情况如何,所以我们就需要获得子进程的退出信息,要获得子进程的退出信息,我们就要使用waitpid函数。
pid_ t waitpid(pid_t pid, int *status, int options); 返回值: 当正常返回的时候waitpid返回收集到的子进程的进程ID; 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0; 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在; 参数: pid: Pid=-1,等待任一个子进程。与wait等效。 Pid>0,等待其进程ID与pid相等的子进程。 status: WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码) options: WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。 若正常结束,则返回该子进程的ID。
我们来写个代码验证一下
运行结果:
这里很奇怪,我们上面给我们的子进程设置的退出码是1,但是经过waitpit获取的退出码却是256,为什么?这是因为status保存了退出码和退出信号两个退出信息,即status有自己的格式。
3.2.3.获取子进程status
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
所以此时次八位存储了一个为1的比特位,表示此时的退出码是1,低八位的最高位我们暂时不使用,由于此时程序没有出现任何异常,程序是正常运行完毕的,所以此时信号编号为0,所以低七位的比特位都是0。那我们的代码要获取退出信息(exit_code和exit_signal),就要使用移位运算符。
运行结果:
此时运行的结果就符合我们的预期效果,那如果我们想看到进程异常的信息编号呢?我们来修改一下我们的代码,我们让我们的子进程运行的时间更长一点,同时退出码设置为0。
待子进程运行了一段时间,我们使用kill -9杀掉我们的子进程。
进程异常的信息编号就是9,此时的代码没有跑完,而是受到异常的信号而终止运行了。如果使用kill -2呢?
此时的进程异常的信息编号就是2,使用 -2
(SIGINT)允许进程优雅地处理终止信号并执行清理任务,而使用 -9
(SIGKILL)会强制终止进程,而不给予其执行任何清理的机会。我们再来看看其他代码异常的情况
运行结果:
在Linux中,异常信号是一种用于通知进程发生了某种事件或错误的机制。每个异常信号都有一个唯一的数字表示,其中8号异常信号对应的是SIGFPE(Floating Point Exception),表示浮点运算错误。当一个进程执行浮点运算时,如果发生了错误,比如试图除以零或者产生了不合法的操作数,就会触发SIGFPE信号。这通常表示程序在执行浮点运算时出现了异常情况,可能是由于代码错误或不当输入引起的。我们再来看看其他代码异常的情况
运行结果:
在Linux系统中,信号是一种用于通知进程发生特定事件的软件中断。每个信号都有一个唯一的数字标识符,而11号信号对应的是SIGSEGV,也称为段错误(Segmentation Fault)。当进程尝试访问无效的内存地址或执行其他违反内存访问规则的操作时,操作系统会向该进程发送SIGSEGV信号。这通常是由于编程错误引起的,例如空指针引用或数组越界访问。难道我们每次获取子进程的退出结果都要进行位操作嘛?不需要,在Linux或类Unix系统中,当一个子进程终止时,父进程可以通过一些宏来检查子进程的终止状态。这些宏包括 WIFEXITED 和 WEXITSTATUS,用于判断子进程是否正常终止以及获取其退出码。
在Linux或类Unix系统中,当一个子进程终止时,父进程可以通过一些宏来检查子进程的终止状态。这些宏包括 WIFEXITED
和 WEXITSTATUS
,用于判断子进程是否正常终止以及获取其退出码。
1.WIFEXITED(status)
:
- 如果这个宏的值为真(非零),则表示子进程正常终止。
- 如果为假(零),则表示子进程非正常终止(比如被信号中断)。
if (WIFEXITED(status)) { // 子进程正常终止 } else { // 子进程非正常终止(异常终止) }
2.WEXITSTATUS(status)
:
- 当
WIFEXITED(status)
为真时,可以使用WEXITSTATUS
宏来提取子进程的退出码。 - 退出码是子进程通过
exit
函数传递给父进程的值,通常用来指示进程的终止状态。退出码的范围通常是 0 到 255。
if (WIFEXITED(status)) { int exit_status = WEXITSTATUS(status); // 使用 exit_status 来获取子进程的退出码 }
这两个宏一般用于处理子进程的退出状态,以便父进程能够根据子进程的终止情况采取相应的措施。这样的处理在编写可靠的、能够应对子进程异常终止的父进程代码中非常重要。
问题:为什么获取子进程的退出信息要使用waitpid这个系统调用,然后还要使用宏获取exit_code和exit_signal,为什么不能直接使用定义exit_code和exit_signal呢?
这是因为我们在父进程两个全局变量exit_code和exit_signal时,当子进程退出时,子进程会对exit_code和exit_signal进行设置,以便父进程拿到,但是此时子进程对数据进行了修改,就会发生写时拷贝,此时父进程拿不到子进程的退出信息。因为进程具有独立性,所有的父子进程无法直接互相修改对方的数据之后,让对方看到,因为读取子进程退出信息,本质时读取内核数据!
在子进程运行期间,我们的父进程正在干嘛呢?父进程在进程阻塞等待,此时父进程什么也没干,那我们就可以让我们的父进程进行非阻塞等待。此时就要涉及到waitpid的第三个参数options
#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options);
其中,options
是 waitpid
的第三个参数,用于指定一些选项,以影响等待子进程的行为。以下是 waitpid
可接受的一些选项:
WNOHANG
:
- 如果指定了
WNOHANG
,waitpid
将以非阻塞方式运行。即使没有子进程退出,它也会立即返回,而不会等待。 - 如果子进程已经退出,
waitpid
返回子进程的PID。如果没有子进程退出,返回0。
在上面的图片当中,张三一直打电话给李四的时候,此时张三什么事情都没有做,而张三在后面轮询期间多次给李四打电话的过程期间,期间每次打一个电话获得一次李四的答复,此时获得答复后张三就可以忙自己的事情,如此往复,张三整个期间就都能做很多的事情,同样的此时父进程就可以在等待子进程退出信息的过程中,可以做其他的事情,现在我们来验证一下。
运行结果:
在我们当前的调度器,父进程是优先于子进程运行的,所以我们会看到父进程的打印输出语句先执行,这里最后输出了两句是因为在子进程在运行循环之后,我们让我们的子进程休眠了一秒,此时子进程的输出工作已经在前5秒已经输出完成了,在上面的例子中,我们明显的观察到了父进程在获取子进程的退出信息过程中,可以做其他的事情,具体可以做那些事呢?我们来举例一下。
运行结果:
四、进程程序替换
4.1.直接说原理 -- 什么是程序替换
首先我们知道我们的程序,只能执行我们的代码,如果我们创建的子进程想执行其他程序的代码呢?此时就要使用我们的进程程序替换。
进程程序替换(Process Program Replacement)是指一个正在运行的进程,通过加载新的程序代码替代原有的程序代码。进程程序替换的优势在于,它允许一个进程在不产生新进程的情况下改变其执行的程序,从而实现动态的程序加载和切换,而这个替换的工作,存在很多的数据变更,而操作系统是软硬件资源的管理者,所以实现替换工作一定会有对应的系统调用接口。
4.2.直接写代码 -- 最简单的单进程的demo代码 -- exec函数的一个接口execl
int excel(const *path, const char *arg, ...)
path: 要执行的程序的路径。这应该是一个以null结尾的字符串,表示要执行的可执行文件的路径。通常,你需要提供程序的绝对路径或相对路径。
arg0, arg1, ..., argn: 这是可执行程序的命令行参数。它们是一个以NULL结尾的字符串列表,其中第一个参数arg0通常是可执行程序的名称。后面的参数是传递给程序的命令行参数。最后一个参数必须是NULL,它标志着参数列表的结束。
下面我就来写一个代码来使用一下excel接口。
运行结果:
此时我们就使用了我们的程序执行了LInux特定的指令ls。但是我们发现我们程序输出的的时候没有输出exec end...,这就说明程序替换一旦成功,excel后续的代码就不再执行,因为被替换掉了,excel函数的返回值是一个整数。在正常情况下,excel函数不会返回,因为它会将当前进程的映像替换为新的程序。如果excel函数返回,这通常表示执行出现了错误。返回值为-1时,表示发生了错误。此时,可以通过查看全局变量errno来获取更多关于错误的信息。errno是一个在头文件 中声明的全局变量,它包含了最后一次系统调用发生错误的错误代码,随后我们将ls的路径修改一下,看看输出的结果
运行结果:
此时程序获取了我们的退出码,那说明excel函数运行肯定失败了。上面我们提到程序替换是没有产生新的进程的,那我们光说也不行呀!先介绍一下top指令。top 是一个在Linux系统下用于动态监视系统运行状态的命令行工具。它提供了实时的系统性能信息,包括 CPU 使用情况、内存使用情况、进程列表等。top 不会自动退出,它会一直运行,定期更新屏幕上的信息。下面代码展示。
运行结果:
此时就验证了我们上面的结论。随后再提出一个问题:创建一个进程,是先创建pcb,还是先加载代码和数据了。这里我们可以通过一个实例说明,我们在高考完之后就会进入一个大写学,在被大学录取之后,是大学先获取我们的个人信息呢?还是我们要先到大学去呢?很明显,肯定是大学先获取我们的个人信息,因为在我们一进入大学之后,我们就知道我们当前所在的班级,以及每周要上什么课程,都已经被学校安排的明明白白了。所以创建一个进程也是如此,先创建我们的pcb,随后在加载我们的程序和代码。上面我们就提到进程程序替换是指一个正在运行的进程,通过加载新的程序代码替代原有的程序代码,所以程序替换做的本质工作就是加载!
【打造你自己的Shell:编写定制化命令行体验】(三):https://developer.aliyun.com/article/1425823