[Linux]:进程(下)

2024-09-09 08:12
文章标签 linux 进程

本文主要是介绍[Linux]:进程(下),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

img

✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:Linux学习
贝蒂的主页:Betty’s blog

1. 进程终止

1.1 进程退出的场景

进程退出只有以下三种情况:

  1. 代码运行完毕,结果正确。
  2. 代码运行完毕,结果不正确。
  3. 代码异常终止(进程崩溃)。

1.2 进程退出码

在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级别代码的入口。main函数是被其他函数间接调用的,例如在VS2013中,main函数由__tmainCRTStartup函数调用,而__tmainCRTStartup函数又是通过加载器被操作系统调用。所以,main函数是间接性被操作系统所调用。

由于main函数是这样被调用的,当main函数调用结束后,应该给操作系统返回相应的退出信息。这个退出信息以退出码的形式作为main函数的返回值返回。一般情况下,我们以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误。这就是为什么我们常在main函数的最后返回0

比如我们执行以下代码:

#include<stdio.h>
int main()
{printf("hello betty!\n");return 0;
}

我们可以通过指令echo $?查看最近一次进程的退出码。

进程正常退出返回0,如果进程不是正常退出就会返回其对应的退出码,在C语言中我们可以通过strerror函数打印出对应的错误信息。

#include<stdio.h>
#include<string.h>
int main()
{for(int i=0;i<150;i++){printf("[%d]->%s\n",i,strerror(i));}return 0;
}

需要注意的是: 退出码都有对应的字符串含义,帮助用户确认执行失败的原因,而这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同。

1.3 exit与_exit函数

首先我们来介绍一下exit函数,其可以在代码中的任何地方退出进程,并且exit函数在退出进程前会做一系列工作:

  1. 执行用户通过atexiton_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入。
  3. 调用_exit函数终止进程。

比如说以下这段代码:

#include<stdio.h>
#include<stdlib.h>
int main()
{printf("hello betty!");exit(1);return 0;
}

如果我们使用的是_exit函数,那么进程就会直接退出,并不会做任何处理。

#include<stdio.h>
#include<unistd.h>
int main()
{printf("hello betty!");_exit(1);return 0;
}

最后我们来谈谈returnexit_exit之间的区别与联系。

首先只有在main函数当中的return才能起到退出进程的作用,子函数当中return不能退出进程,而exit函数和_exit函数在代码中的任何地方使用都可以起到退出进程的作用。

使用exit函数退出进程前,exit函数会执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作。

画板

其中执行return num等同于执行exit(num),因为调用main函数运行结束后,会将main函数的返回值当做exit的参数来调用exit函数。

2. 进程等待

为了解决僵尸进程,获取子进程的退出信息我们需要使用进程等待。其中进程等待有两个关键的函数waitwaitpid

2.1 status参数

其中这两个关于进程等待的函数都有一个共同的参数status,如果对status参数传入NULL,表示不关心子进程的退出状态信息。否则,操作系统会通过该参数,将子进程的退出信息反馈给父进程。

*status虽然是一个整型变量,但*status不能简单的当作整型来看待,因为status的不同比特位所代表的信息不同,一般我们只考虑低的16个比特位。

*status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。

画板

一般我们可以通过相关的位运算得到进程的退出码与退出信号。

exitCode = (status >> 8) & 0xFF; //退出码 11111111
exitSignal = status & 0x7F;      //退出信号 01111111

为了降低用户的使用成本,操作系统也为我们提供了两个宏表示对应的退出码与退出信号。

  • WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
  • WEXITSTATUS(status):用于获取进程的退出码。

2.2 wait函数

我们首先来介绍一下wait函数的使用的方法:

  • 原型:pid_t wait(int* status);
  • 参数:输出型参数,获取子进程的退出状态,不关心可设置为NULL
  • 返回值:等待成功返回被等待进程的pid,等待失败返回-1。

比如下面这段代码,我们用父进程一直等待子进程,然后获取其退出信息。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{pid_t id = fork();//创建子进程if(id==0){//chlldint count=10;while(count--){printf("I am child:PID:%d,PPID:%d\n",getpid(),getppid());sleep(1);}exit(0);}//fatherint status=0;pid_t ret=wait(&status);//如果等待成功if(ret>0){printf("wait child success\n");if(WIFEXITED(status)){//退出正常printf("exit code:%d\n",WEXITSTATUS(status));}else{printf("exit signal:%d\n",status&0x7f);}}sleep(10);return 0;
}

子进程正常退出,父进程成功获取退出信息,子进程就不会形成僵尸进程。

如果我们通过指令杀死进程,父进程同样能等待成功并返回对应的信号。

2.3 waitpid函数

同样我们再来介绍waitpid函数。

  • 原型:pid_t waitpid(pid_t pid, int* status, int options);
  • 返回值:等待成功返回被等待进程的pid。如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程,则返回0。如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
  • 参数:pid:待等待子进程的pid,若设置为-1,则等待任意子进程。status:输出型参数,获取子进程的退出状态,不关心可设置为NULLoptions:当设置为WNOHANG时,若等待的子进程没有结束,则waitpid函数直接返回0,不予以等待。若正常结束,则返回该子进程的pid

例如,创建子进程后,父进程可使用waitpid函数一直等待子进程(此时将waitpid的第三个参数设置为0),直到子进程退出后读取子进程的退出信息。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{pid_t id = fork();if (id == 0){//child          int count = 10;while (count--){printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());sleep(1);}exit(0);}//father           int status = 0;pid_t ret = waitpid(id, &status, 0);if (ret >= 0){//wait success                    printf("wait child success...\n");if (WIFEXITED(status)){//exit normal                                 printf("exit code:%d\n", WEXITSTATUS(status));}else{//signal killed                              printf("eixt siganl %d\n", status & 0x7F);}}sleep(10);return 0;
}

并且我们还可以使用创建等待多进程的方式。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{pid_t ids[10]={0};for (int i = 0; i < 10; i++){pid_t id = fork();if (id == 0){//childprintf("child process created successfully...PID:%d\n", getpid());sleep(3);exit(i); //将子进程的退出码设置为该子进程PID在数组ids中的下标}//fatherids[i] = id;}for (int i = 0; i < 10; i++){int status = 0;pid_t ret = waitpid(ids[i], &status, 0);if (ret >= 0){//wait child successprintf("wait child success..PID:%d\n", ids[i]);if (WIFEXITED(status)){//exit normalprintf("exit code:%d\n", WEXITSTATUS(status));}else{//signal killedprintf("exit signal %d\n", status & 0x7F);}}}return 0;
}

2.4 非阻塞轮询

在传统的父子进程关系中,当子进程未退出时,父进程通常处于阻塞等待状态,在此期间父进程不能进行其他操作。

然而,我们可以采用非阻塞等待的方式。具体做法是在调用waitpid函数时,向第三个参数options传入WNOHANG。这样,如果等待的子进程没有结束,waitpid函数将直接返回 0,父进程不进行等待,可以去做自己的事情。而当等待的子进程正常结束时,waitpid函数会返回该子进程的pid,此时父进程可以读取子进程的退出信息。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{pid_t id=fork();//创建子进程if(id==0){//childint count=3;while(count--){printf("child do something\n");sleep(3);}exit(0);}//fatherwhile(1){int status=0;pid_t ret=waitpid(id,&status,WNOHANG);if(ret>0){printf("wait success\n");printf("exit code:%d\n",WEXITSTATUS(status));break;}else if(ret==0){printf("father do other things\n");sleep(1);}else{//wait errorbreak;}}return 0;
}

3. 进程替换

3.1 进程替换的概念

我们前面知道,父子进程是共享代码与数据的,如果修改子进程的数据就会发生写实拷贝。而今天我们需要修改子进程的代码,则需要进行进程替换

当进程替换时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行。

画板

如果父子进程共享数据与代码,当对子进程进行进程替换时就会发生写实拷贝,所以对子进程就行进程替换并不会影响父进程。

3.2 进程替换函数

进程替换可以使用一下六个函数:

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则返回-1。

首先我们要介绍的是execl函数:

  • 原型:int execl(const char *path, const char *arg, …)
  • 参数:path是要执行程序的路径,arg是可变参数列表,表示你要如何执行这个程序, 注意以NULL为参数传递的结尾。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>int main() 
{pid_t id = fork();if (id < 0) {perror("fork failed");return 1;} else if (id == 0) {// childif (execl("/usr/bin/ls","ls","-l","-a", NULL) == -1) {perror("execl failed");exit(-1);}} else {// fatherint status;pid_t ret = waitpid(id, &status, 0);if (ret < 0) {perror("waitpid failed");return 1;}if (WIFEXITED(status)) {printf("wait success\n");printf("exit code:%d\n", WEXITSTATUS(status));}}return 0;
}

并且我们也可能通过该接口,调用其他语言的脚本,比如我们调用一个python脚本。

execl("/usr/bin/python3","python","testp.py", NULL)

接下来我们将学习execlp接口:

  • 原型:int execlp(const char *file, const char *arg, …);
  • 参数:file是要执行程序的名称,arg是可变参数列表,表示你要如何执行这个程序, 注意以NULL为参数传递的结尾。
execlp("ls", "ls", "-a", "-l", NULL);//执行ls -a -l

继续学习execle接口:

  • 原型:int execle(const char *path, const char *arg, …, char *const envp[]);
  • 参数:path是要执行程序的路径,arg是可变参数列表,表示你要如何执行这个程序, 注意以NULL为参数传递的结尾,envp是你自己设置的环境变量。

例如,你设置了MYVAL环境变量,在mycmd程序内部就可以使用该环境变量。

char* myenvp[] = { "MYVAL=2021", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);//执行./mycmd

继续学习execv接口:

  • 原型:int execv(const char *path, char *const argv[]);
  • 参数:path是要执行程序的路径,argv是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾。
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);//执行ls -a -i -l

剩余接口我们就不在介绍了,因为每个接口的参数都类似,如果学习完上面接口,剩余接口的使用也是零成本。

最后为了方便记忆,我们将这些接口归类形成如下表格:

替换函数接口后后缀含义
l(list)参数采用列表方式
v(vector)参数采用数组方式
p(path)自动搜索环境变PATH,进行程序查找
e(env)自己维护环境变量,或者说自定义环境变量,可以传入自己设置的环境变量
函数名参数格式是否带路径是否使用当前环境变量
execl列表
execlp列表
execle列表否,需自己组装环境变量
execv数组
execvp数组
execve数组否,需自己组装环境变量

事实上,在系统调用中,只有execve才是真正的系统调用,其他五个函数(如execlexecleexeclpexecvexecvp)都是对execve函数的封装,目的是为了满足不同用户的需求。这也导致了在man手册中,execve位于第 2 节,而其他五个函数在第 3 节。

这篇关于[Linux]:进程(下)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1150622

相关文章

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Linux服务器Java启动脚本

Linux服务器Java启动脚本 1、初版2、优化版本3、常用脚本仓库 本文章介绍了如何在Linux服务器上执行Java并启动jar包, 通常我们会使用nohup直接启动,但是还是需要手动停止然后再次启动, 那如何更优雅的在服务器上启动jar包呢,让我们一起探讨一下吧。 1、初版 第一个版本是常用的做法,直接使用nohup后台启动jar包, 并将日志输出到当前文件夹n

【Linux】应用层http协议

一、HTTP协议 1.1 简要介绍一下HTTP        我们在网络的应用层中可以自己定义协议,但是,已经有大佬定义了一些现成的,非常好用的应用层协议,供我们直接使用,HTTP(超文本传输协议)就是其中之一。        在互联网世界中,HTTP(超文本传输协议)是一个至关重要的协议,他定义了客户端(如浏览器)与服务器之间如何进行通信,以交换或者传输超文本(比如HTML文档)。

如何编写Linux PCIe设备驱动器 之二

如何编写Linux PCIe设备驱动器 之二 功能(capability)集功能(capability)APIs通过pci_bus_read_config完成功能存取功能APIs参数pos常量值PCI功能结构 PCI功能IDMSI功能电源功率管理功能 功能(capability)集 功能(capability)APIs int pcie_capability_read_wo

Linux操作系统 初识

在认识操作系统之前,我们首先来了解一下计算机的发展: 计算机的发展 世界上第一台计算机名叫埃尼阿克,诞生在1945年2月14日,用于军事用途。 后来因为计算机的优势和潜力巨大,计算机开始飞速发展,并产生了一个当时一直有效的定律:摩尔定律--当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。 那么相应的,计算机就会变得越来越快,越来越小型化。