【Linux】——进程(下)

文章目录

进程创建

创建进程其实就是使用fork(),fork()有两个返回值。如果创建成功就返回给父进程返回子进程的pid,给子进程返回0;创建失败就给父进程返回-1。

因为有虚拟地址,父子进程看似代码和数据共享,其实会写时拷贝,在子进程修改数据时会开辟新空间。

进程终止

进程退出时的三种情况

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

exit 和 _exit 函数

exit和_exit都会直接结束进程,不会执行后续任何代码

我们整个代码来看看exit和_exit的差别:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int main() {
    printf("Hello");  // 无换行符,缓冲区未刷新
    // exit(0);        // 会触发清理和刷新缓冲区
    _exit(0);         // 直接退出,不执行清理和刷新
    return 0;
}

exit和exit的不同点

exit是c标准库,_exit是系统调用

exit执行刷新缓冲区,_exit不执行

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

进程等待

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

cpp 复制代码
#include<sys/types.h>
#include<sys/wait.h>
 
pid_t wait(int*status);
 
//成功返回被等待进程pid,失败返回-1
//返回的进程pid是随机的,如果有多个被等待的pid,它会随机选择一个
//status是输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

由上wait方法我们看到有个参数叫status,waitpid也有。

status

如果给status参数传NULL,表示会正常等待子进程结束,但是不获取子进程的退出状态。

否则操作系统会通过statu的参数,将子进程的退出信息反馈给父进程。

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

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

如果想要读取status退出状态或者终止信号的话,可以通过两个宏

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

WEXITSTATUS(status): 用于获取进程的退出码。

wait

wait的方法:

cpp 复制代码
#include<sys/types.h>
#include<sys/wait.h>
 
pid_t wait(int*status);
 
//成功返回被等待进程pid,失败返回-1
//返回的进程pid是随机的,如果有多个被等待的pid,它会随机选择一个
//status是输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

我用一段代码演示父进程等待子进程结束然后获得子进程退出信息:

cpp 复制代码
#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)
    {
        //chlld
        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=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;
}

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

如果杀死了子进程,父进程一样是可以等待成功获取退出信号的

waitpid

waitpid方法:

cpp 复制代码
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int*status, int options);
//返回值和status都与wait是一样的,可以说wait是waitpid封装的一个函数,当pid==-1,option==0,那么waitpid==wait

//第一个参数pid:
//pid > 0:等待进程 ID 等于 pid 的子进程
//pid == -1:等待任意子进程
//pid == 0:等待与调用进程(父进程)属于同一进程组的所有子进程

//第三个参数options:
//options == 0,意味着这个选项没有任何用处,未开启选项
//options == WNOHANG,若pid指定的子进程没有结束,则waitpid直接返回0,不等待,若正常结束,则返回子进程pid 

用以下代码演示一下waitpid获取子进程退出码:

cpp 复制代码
#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;
}

非阻塞轮询

在之前的父子进程关系中,当子进程还未退出时,父进程通常处于阻塞状态,这个期间父进程不能进程其他操作。

但是,我们可以采取非阻塞等待的方式。其实就是在调用waitpid时,给第三个参数optionsWNOHANG

//options == WNOHANG,若pid指定的子进程没有结束,则waitpid直接返回0,不等待,若正常结束,则返回子进程pid

不等待父进程就可以去做其他事。当被等待的子进程正常结束时,父进程就可以拿到子进程的退出信息:

cpp 复制代码
#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)
    {
        //child
        int count=3;
        while(count--)
        {
            printf("child do something\n");
            sleep(3);
        }
        exit(0);
    }
    //father
    while(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 error
            break;
        }
    }
    return 0;
}

进程替换

什么是进程替换:

共享代码和数据的父子进程,在修改子进程时就会发生写时拷贝。如果要修改子进程的代码,则需要进程替换

由下图可以了解到:

进程替换不是创建新的进程,仅仅是将代码和数据替换罢了,进程相关的PCB信息没有任何改变。

进程替换函数

进程替换函数可以让不同语言编写的程序串联起来!

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

execl:执行指定路径的程序,参数以可变参数形式传递,必须以NULL结尾。

execlp:在系统的PATH环境变量中搜索程序并执行,参数以可变参数形式传递。

execle:执行指定路径的程序,允许传递环境变量数组envp,用于指定新程序的环境变量。

execv:执行指定路径的程序,参数以数组形式传递。

execvp:在系统的PATH环境变量中搜索程序并执行,参数以数组形式传递。

execvpe:在系统的PATH环境变量中搜索程序并执行,允许传递环境变量数组envp。

cpp 复制代码
//库函数,它们6个最终都是调用的execve
#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
//系统调用函数
int execve(const char *path, char *const argv[], char *const envp[]);

通过以上的介绍,我们可以发现它们的命名规律:

l: list,表示中间参数采用列表

p: path,自动搜索环境变量PATH

e: env,自己维护环境变量

v: vector,表示中间参数用数组

从最简单的execl函数介绍:

它的第一个参数就是路径,第一个参数到最后一个参数之间的参数叫做可变参数列表,也就是说可以有多个,它和我们在命令行使用的命令是一样的,只不过我们命令行的分隔是空格,而这里的分隔是逗号和引号,最后一个参数必是NULL,表示该表的末尾边界

execlp:

除了第一个参数和execl不一样以外,其他的格式都一样,p说明它会先从环境变量PATH中找,所以我们要执行命令可以不用带路径,直接写命令名字就行了,用execlp执行 ls -l -a 命令的等效代码就是:
execlp("ls","ls","-l","-a",NULL);

execle:

execle函数,除了比execl多了一个最后一个参数以外,其他都不变,只是NULL值要在倒数第二个参数,同上方的代码应该是如下所示:
extern char** environ; execlp("/usr/bin/ls","ls","-l","-a",NULL,envrion);

传入execle的环境变量表也可以用用户定义的:

c 复制代码
char* const my_environ[]
{	
	"SUPER=1",
	"LITTLE=2",
	"MONSTER=3",
	NULL	
};
execle("/usr/bin/ls","ls","-l","-a",NULL,my_environ);
c 复制代码
int main()
{
    printf("my process\n");
    char* const my_environ[]=
    {
        "SUPER=1",
        "LITTLE=2",
        "MONSTER=3",
        NULL
    };
    execle("/usr/bin/ls","ls","-l","-a",NULL,my_environ);
    printf("my process\n");
    return 0;
}

看起来和命令相同,实际上用户自定义的环境变量已经成为替换进程的一部分了。

这三个进程替换函数掌握了,剩下的接口参数都是类似的,想要自定义参数也是一样的操作。

相关推荐
9毫米的幻想几秒前
【Linux系统】—— 进程状态
linux·运维·服务器
花落已飘3 分钟前
RK3568 pinctrl内容讲解
linux·设备树·rk3568
白悠离24 分钟前
hackmyvm-JO2024
linux·web安全·网络安全
SecPulse43 分钟前
Linux安装Cmake (Centos 7.9)
linux·运维·centos·cmake·流影
阿巴~阿巴~1 小时前
Day1 蓝桥杯省赛冲刺精炼刷题 —— 位运算与循环(2)
c语言·c++·算法·蓝桥杯
Tee xm1 小时前
清晰易懂的Rust安装与配置教程
linux·windows·macos·rust
十六ᵛᵃᵉ2 小时前
day3_Flink基础
android·java·flink
菜_小_白2 小时前
gdb调试
linux·c语言·c++
千航@abc2 小时前
深度剖析 ansible:从部署基础到模块运用及剧本编写
运维·centos·ansible
小六子成长记2 小时前
C语言笔记数据结构(链表)
c语言·数据结构·链表