第三部分:5---进程等待、进程终止

目录

进程的两种终止方式:

正常终止——进程退出码:

查看最近一次进程退出的退出码:

自定义退出码对应的文本信息:

退出码和C语言的错误码的关系:

异常终止——操作系统发送信号:

————————————————

exit 和 _exit终止进程的区别:

————————————————

什么是进程等待?

为什么要等待进程?

wait实现进程等待:

waitpid实现进程等待:

对status的解析:

为什么不写一个status全局变量,直接接收子进程的退出码?

对options的解析 / 进程的非阻塞等待:


进程的两种终止方式:

  • 正常终止:当进程完成其所有任务后,自行结束,并返回一个退出码。这个退出码通常通过 main 函数的返回值或 exit() 函数指定。正常终止的进程会将退出码传递给操作系统和父进程,用于告诉其结束原因。

  • 异常终止:当进程在运行过程中因某种原因未能正常完成而被迫终止。这通常是由于进程收到来自操作系统的信号,例如 SIGKILL 或 SIGSEGV。信号可能由用户(用户可以手动向进程发送信号使其终止,例如:“ kill -9 pid ”)、操作系统或其他进程触发,表示进程需要立即停止。异常终止的进程可能不会有机会返回一个正常的退出码,信号本身就代表了进程的终止原因。

  • 正常终止和异常终止的区别在于,正常终止是进程在预期的条件下完成工作并返回退出码,而异常终止则是由于外部或内部的意外情况导致的强制结束。

正常终止——进程退出码:

  • main 函数的返回值(退出码)会传递给父进程,表示子进程的退出状态。这一返回值可以告诉父进程子进程是如何结束的,并指示其执行结果是否正确。

  • 返回值为 0 表示子进程正常结束,这通常表示程序执行成功,没有遇到任何错误。

  • 返回值为非 0 表示子进程异常结束。不同的非零值代表不同的错误原因或异常情况。程序可以通过返回特定的数字值来表明特定的错误类型,从而为父进程提供详细的错误信息。

  • 纯数字的返回值虽然简洁,但在表达错误原因时可能不够直观。Unix/Linux 系统中的 strerror 函数可以将错误代码转换为对应的错误描述字符串。这种机制使得程序能够有效地报告错误原因,并帮助开发者或父进程做出相应的处理,如记录日志、重试操作或采取其他补救措施。

for(int i=0;i

查看最近一次进程退出的退出码:

echo $? //打印最近一个进程退出时的退出码,这个退出码存放在?中

自定义退出码对应的文本信息:

const char* err_string[]={"SUCCESS" , "OPER ERROR"};//定义了两个退出码(1和2)对应的信息

退出码和C语言的错误码的关系:

  • 在 C语言 中,有一个全局变量 errno,它用于获取最近一次函数调用出错时的错误码。errno 通常在库函数或系统调用出错时被设置,以提供关于错误原因的详细信息。

  • errno 返回的错误码 主要反映库函数或系统调用在执行过程中出现的问题。例如,如果文件打开失败,errno 可能会被设置为 ENOENT,表示文件不存在。通过检查 errno,程序可以了解调用失败的具体原因,并采取相应的处理措施。

  • 退出码(exit status) 是进程退出时返回给操作系统的一个状态值,用于指示进程的退出情况。main 函数的返回值或通过 exit() 函数指定的值将成为退出码。退出码为 0 通常表示进程成功结束,非零值则表示进程以某种错误或异常状态结束。

  • 虽然 errno 和退出码的作用不同,但它们的共同目的是在函数调用或进程执行完成后,提供关于出错原因的详细信息。errno 主要用于函数级别的错误处理,而退出码则用于进程级别的状态报告。

//将程序的退出码与系统的错误码关联起来,在函数调用失败后,可以直接打印得到失败的原因。

int main()
{
	int ret=0; 
	FILE* file=fopen("./111.txt","w"); //打开一个不存在的文件,file会被设置为nullptr
	if(file==nullptr)
	{
		ret=errno; //将返回值设置为errno的出错码
	}
	return ret; //返回出错吗,可以直接得知出错原因
}

异常终止——操作系统发送信号:

  • 如果进程异常终止,这意味着进程的代码未能正常执行完毕,因此退出码对于这种情况没有意义。退出码只在进程顺利完成其任务并正常退出时才有意义,能够反映执行结果的状态。

  • 当进程出现异常时,操作系统会主动终止该进程,通常是通过向进程发送信号来实现。常见的信号包括 SIGKILL(强制终止)和 SIGSEGV(段错误)。这些信号会立即中断进程的执行,迫使其停止运行。

  • 进程异常的本质在于它收到了操作系统发来的信号,并因此自行终止。信号的发送通常意味着进程执行过程中发生了严重错误或不允许的操作,操作系统通过发送信号来防止问题进一步扩展。

  • 因此,判断一个进程是否异常终止,只需要查看该进程是否收到了操作系统发出的信号即可。信号的接收与处理是判断进程是否因异常情况被终止的关键依据。

————————————————

exit 和 _exit终止进程的区别:

  • exit 函数属于 C 标准库,用于终止进程并返回到操作系统。exit 会执行一系列的清理操作,包括自动刷新标准 I/O 流的缓冲区,以确保所有缓冲的数据被写入到目标文件或设备。

  • _exit 是一个系统调用,直接终止进程,不进行任何额外的清理操作。 _exit 直接从内核返回,跳过了标准库的缓冲区处理机制。因此,它不会刷新缓冲区,任何尚未写入的数据都会被丢弃。

  • exit 是对 _exit 的封装,在 _exit 的基础上增加了额外的功能,包括刷新所有的缓冲区、关闭文件描述符等。这些操作确保了在进程退出之前,所有相关资源都被正确处理。

  • 为什么 _exit 不会刷新缓冲区而 exit 会?原因在于:缓冲区的管理是在 C 标准库中完成而不是操作系统内部。 _exit 是直接与操作系统交互的系统调用,它跳过了标准库的清理步骤,因此不能刷新缓冲区。而 exit 函数则会调用标准库中的清理函数来处理缓冲区数据,因为缓冲区管理是由 C 标准库负责的,而不是操作系统直接处理的。

第三部分:5---进程等待、进程终止_第1张图片

————————————————

什么是进程等待?

  • 进程等待就是通过 wait / waitpid 的方式,让父进程等待子进程终止后,对子进程回收资源的过程。

为什么要等待进程?

  • 在子进程结束后,父进程需要等待子进程的终止,以释放变为僵尸进程的子进程,从而防止系统资源的泄漏。

  • 父进程等待子进程结束的原因之一,是为了获取子进程的退出码。这个退出码提供了子进程执行任务后的状态和信息,使父进程能够了解子进程是否成功完成了其任务,或者发生了何种错误。基于这些信息,父进程可以采取相应的措施,比如记录日志、重试操作、或处理错误等。

wait实现进程等待:

pid_t wait(int *status);
//int *status:指向一个整数变量的指针,用于存储子进程的退出状态。如果你不关心退出状态,可以传递 NULL。
//成功时,wait返回终止的子进程的进程 ID。
//如果调用时没有子进程,则返回 -1,并设置 errno 以指示错误类型。

int status;
pid_t child_pid = wait(&status);  // 等待子进程终止
  • 等待任意一个子进程结束后返回。

  • 父子进程,哪一个先被调用,操作系统直到我们不知道。但是父子进程哪一个先结束,必定是父进程要等待子进程结束,释放其僵尸状态的PCB。

waitpid实现进程等待:

pid_t waitpid(pid_t pid, int* status, int options);
//返回值如果大于0,表示等待成功可以下一步操作了。
//返回值如果小于0,表示子进程等待失败。
//返回值如果等于0,表示子进程还在运行没有退出。
//等待指定pid的子进程结束后执行。pid被设置为-1后,作用变为等待任意一个子进程结束。
//options被设置为0,表示-阻塞等待。

对status的解析:

  • status 是一个输出类型参数,通常用于 wait() 或 waitpid() 系统调用,用来接收子进程的退出状态。

  • status 是一个 int 类型的变量,包含了 32 个比特位。这些比特位用于存储子进程的退出码、退出信号以及其他与进程终止相关的信息。

第三部分:5---进程等待、进程终止_第2张图片

 

  • 当子进程终止时,它会将自己的退出码和退出信号信息写入到自己的进程控制块(PCB)中。父进程通过调用 wait() 或 waitpid() 系统调用,能够获取子进程的退出信息。这两个系统调用会将子进程的退出码和退出信号提取出来,并将这些信息写入到 status 变量中,从而让父进程可以读取和处理。

WIFEXITED(status) //得到进程退出码

为什么不写一个status全局变量,直接接收子进程的退出码?

  • 根据之前对虚拟地址和物理地址的分析可知,在父子进程之间,即使全局变量在同一份代码中定义,它们在内存中的物理地址也不会相同。虽然父子进程共享相同的代码和相同的虚拟地址空间布局,但它们的全局变量在实际物理内存中会被映射到不同的物理地址上。

  • 因此,父进程无法直接访问子进程的数据。由于父子进程的全局变量映射到不同的物理地址上,父进程无法通过简单的变量访问来获取子进程的数据。

  • 要在父子进程之间传递数据,需要通过操作系统提供的系统调用来实现。例如,父进程可以通过 wait()、waitpid() 等系统调用获取子进程的退出状态,也可以通过管道(pipe)、共享内存(shared memory)、消息队列(message queue)等进程间通信机制来传递更复杂的数据。

对options的解析 / 进程的非阻塞等待:

  • 当将传递给 waitpid 的 options 参数设为 0 时,意味着 waitpid 采用的是“阻塞等待”模式。在这种模式下,父进程会一直阻塞,直到有一个子进程终止为止。在阻塞期间,父进程无法执行其他任务,必须等待子进程的状态变化。

  • 如果传递给 options 的值为宏 WNOHANG,则 waitpid 将采用“非阻塞等待”模式。在这种模式下,如果没有子进程立即终止,waitpid 会立即返回,并不会阻塞父进程。父进程可以继续执行其他操作,并在适当的时机再次调用 waitpid 来检查子进程的状态。

  • 常见的非阻塞等待方案就是“非阻塞轮询”,即父进程通过定期调用 waitpid 并结合 WNOHANG 宏来检查子进程的状态。轮询指的是父进程反复检查子进程状态的行为,而非阻塞则确保了在子进程未终止时,父进程不会被阻塞,可以继续执行其他任务。

  • 相比于阻塞等待模式下父进程什么都不能做,非阻塞等待使父进程可以在等待子进程终止的同时处理其他任务。这种方式有效提高了父进程的效率,特别是在需要处理多个子进程或需要持续执行其他任务的情况下。  

你可能感兴趣的:(Linux,CentOS,算法,服务器,linux)