【Linux】进程控制(进程创建、进程终止、进程等待、进程替换)

发布于:2025-05-19 ⋅ 阅读:(13) ⋅ 点赞:(0)

目录

一、进程创建

1、fork函数

2、页表权限

二、进程终止

1、main函数返回值(退出码)

2、常见错误码及其对应的错误描述:

将错误退出码转化为错误描述的方法:

3、进程退出的三种场景

4、由上我们可以知道:

5、exit和_exit

三、进程等待

1、为什么进行进程等待

2、进程等待的重要性:

3、wait函数和waitpid函数

wait测试代码:

waitpid测试代码以及参数status解析:

参数status:

4、阻塞与非阻塞

四、进程程序替换

1、什么是程序替换?

2、测试代码,运用exec*系列的系统调用:

3、几个细节

4、更改为多进程版本

5、学习各种exec*接口

(1)execlp

(2)execv

(3)、execve(唯一的系统调用)

6、问题:exec*可以替换系统指令,那可以替换成我们自己的程序吗?

7、关于程序替换与环境变量的知识:

观察现象

如果我们putenv函数新增环境变量呐?现象是什么?

如果我们想设置全新的环境变量给子进程呐?(execle接口)

8、exec*系列接口结构图:


前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家

点击跳转到网站

一、进程创建

1、fork函数

前面我们已经知道了,创建一个子进程可以使用fork函数,现在我们可以复习一下fork函数。

(1)头文件:#include <unistd.h>
(2)函数原型:pid_t fork(void);
(3)返回值:子进程中返回0,父进程返回子进程pid,出错返回-1;
进程调用fork,当控制转移到内核中的fork代码后,内核做:
(1)分配新的内存块和内核数据结构给子进程
(2)将父进程部分数据结构内容拷贝至子进程
(3)添加子进程到系统进程列表当中
(4)fork返回,开始调度器调度

2、页表权限

上一章节,我们提到页表的概念,其实页表内容除了有虚拟地址和物理地址,后面还有很多选项,比如页表权限:

比如说C语言里面的字符串常量具有常属性,是不能被修改的,实际上是虚拟地址映射物理地址时的页表权限为只读权限

二、进程终止

1、main函数返回值(退出码)

平时我们写C程序时,main函数都会return 0,以前都不懂为什么要return 0,下面我们来讲解:

main函数的返回值又叫退出码:

一般情况:

(1)返回0,表示进程执行成功;

(2)返回非零,表示进程执行失败;

非零情况具体到不同数字表示不同的失败原因。

注意:只有在main函数return返回时,才表示进程退出,此时的return xxx 才表示退出码,其他函数return时仅仅表示该函数调用完成。

2、常见错误码及其对应的错误描述:

常见错误码及其含义:
错误码 1: Operation not permitted
错误码 2: No such file or directory
错误码 3: No such process
错误码 4: Interrupted system call
错误码 5: Input/output error
错误码 6: Device not configured
错误码 7: Argument list too long
错误码 8: Exec format error
错误码 9: Bad file descriptor
错误码 10: No child processes
错误码 11: Resource temporarily unavailable
错误码 12: Cannot allocate memory
错误码 13: Permission denied
错误码 14: Bad address
错误码 15: Block device required

将错误退出码转化为错误描述的方法:

(1)、使用语言和系统自带的方法进行转化:strerror

我们以错误码2为例:

错误码2对应的描述为:"No such file or directory",

宏定义为:ENOENT

(2)我们自定义

#include<stdio.h>
enum
{
	success = 0,
	open_err,
	malloc_err
};

const char* errorToDesc(int code)
{
	switch(code)
	{
		case success:
			return "success\n";
		case open_err:
			return "file open error\n";
		case malloc_err:
			return "malloc error\n";
		default:
		return "unknown error\n";
	}
}

int main()
{
	printf("%s",errorToDesc(0));
	printf("%s",errorToDesc(1));
	printf("%s",errorToDesc(2));
	return 0;
}

3、进程退出的三种场景

(1)进程代码执行完毕,结果也是正确的;

(2)进程代码执行完毕,但结果不正确;

(3)进程代码没有执行完,进程异常退出了(收到了异常信号)。

第(3)钟情况一般是进程收到了异常信号,每个信号都有不同编号,不同编号又表示不同的异常原因,如下:

#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>

//进程收到信号的场景
int main()
{
	while(1)
	{
		printf("process is running,pid: %d\n",getpid());
		sleep(1);
	}
	return 0;
}

4、由上我们可以知道:

任何进程最终的执行情况,我们可以使用两个数字表明具体执行情况:

(1)信号码(signumber)

(2)退出码(exit_code)

5、exit和_exit

(1) exit和_exit都是用来终止进程的,在任意一个地方调用都会终止进程,其参数就是进程的退出码。

(2)两者关系:exit是库函数,_exit是系统调用,exit封装了_exit。所以推荐使用exit,因为语言接口具有跨平台性,并且系统调用的开销会更大。

(3)当我们运行程序发现exit会刷新缓冲区,而_exit不会刷新缓冲区。所以我们之前所谈的缓冲区,比如进度条程序的缓冲区绝对不是操作系统里面的缓冲区,若是操作系统的里面的缓冲区,那么_exit系统调用就会刷新缓冲区。后面学习我们会知道,这里缓冲区是在标准库里面(也叫上层缓冲区)

三、进程等待

1、为什么进行进程等待

(1)父进程需要通过wait方式,去回收子进程的资源;

(2)父进程通过wait方式可以获取子进程的退出信息;

2、进程等待的重要性:

(1)之前讲过,子进程退出时,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就会“刀枪不入”,即使是信号9也杀不掉,因为僵尸状态的进程实际上已经死了。

(2)最后,我们还需要知道父进程派给子进程的任务完成的如何,结果对还是不对,有没有正常退出等等信息,父进程就是通过进程等待的方式,回收子进程资源来获取这些退出信息的。

3、wait函数和waitpid函数

(1)wait() 和 waitpid() 都是用于父进程等待子进程终止的系统调用;

特性 wait waitpid
函数原型

pid_t wait(int *status);

pid_t waitpid(pid_t pid, int *status, int options);
等待范围 等待任意一个子进程终止。 可精确指定等待某个子进程(通过 pid 参数)或任意子进程。
阻塞行为 阻塞:若无子进程终止,父进程会挂起。 除了左述,可通过 WNOHANG 选项设置为非阻塞,立即返回。
状态获取 通过 status 参数获取子进程退出状态。 同 wait(),但支持更多状态检查宏(如 WIFEXITED()WIFSIGNALED())。
僵尸进程处理 回收终止子进程的资源(避免僵尸进程)。 同 wait(),但可选择性回收(如通过 WNOHANG)。
适用场景 父进程只需等待任一子进程结束,无需控制细节。 需精确控制等待的子进程(如多子进程场景),或需要非阻塞等待。
返回值:

wait测试代码:

#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include<stdlib.h>

//wait的测试代码
int main()
{
	pid_t id = fork();
	if(id == 0)
	{
		//child
		int cnt = 5;
		while(cnt)
		{
			printf("Child is running,pid: %d,ppid: %d\n",getpid(),getppid());
			sleep(1);
			cnt--;
		}
		printf("子进程准备退出,马上变成僵尸进程\n");
		exit(0);

	}
	printf("父进程sleep中.......\n");
	sleep(10);
	//father
	pid_t rid = wait(NULL);//阻塞等待
	if(rid > 0)
	{
		printf("wait success,rid: %d\n",rid);
	}
	printf("父进程回收僵尸成功\n");
	sleep(3);
	return 0;
}

注意:wait进程的是阻塞等待。fork之后,父子进程谁先运行是不确定的,这是由调度器说了算,所以刚开始我们让父进程休眠10秒。

waitpid测试代码以及参数status解析

#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include<stdlib.h>


//waitpid测试代码
int main()
{
	pid_t id = fork();
	if(id == 0)
	{
		//child
		int cnt = 5;
		while(cnt)
		{
			printf("Child is running,pid: %d,ppid: %d\n",getpid(),getppid());
			sleep(1);
			cnt--;
		}
		printf("子进程准备退出,马上变成僵尸进程\n");
		exit(1);

	}
	printf("父进程sleep中.......\n");
	sleep(10);
	int status = 0;
	pid_t rid = waitpid(id,&status,0);//阻塞等待
	if(rid > 0)
	{
		printf("wait success, rid: %d,status: %d\n",rid,status);
	}
	printf("父进程回收僵尸成功\n");
	sleep(3);
	return 0;
}

参数status:

我们发现wait和waitpid中都有一个参数status:该参数是一个整形指针,我们可以创建一个整形变量传进去获取对应信息。

实际上status里面保存了进程的退出码和退出信号,但如上图,我们获取的status是256是怎么回事?

因为status有自己的格式,我们需要通过对status解析才能获取我们想要的信息。

参数status是一个整形:

(1)最低7位保存的是信号编号;

(2)第8位目前不用管;

(3)次8位保存的退出码;

上述我们获取的status的值为256,所以解析如下:

所以我们可以通过移位运算符去解析该参数得到对应的退出码和退出信号:

我们也可以通过宏去解析:注意下述宏都包含在头文件:<sys/wait.h> 

4、阻塞与非阻塞

(1)我们要知道wait只有阻塞等待模式;

(2)waitpid既有阻塞模式,又有非阻塞模式;

waitpid通过第三个参传入宏:WNOHANG,即可变为非阻塞模式;

所以waitpid的非阻塞模式可以进行轮询多个子进程:

while (1) {
    pid_t pid = waitpid(-1, &status, WNOHANG);
    
    if (pid == 0) {
        // 无子进程终止,处理其他任务
        handle_other_tasks();
        continue;
    } else if (pid > 0) {
        // 处理已终止的子进程
        printf("子进程 %d 已终止\n", pid);
    } else if (pid == -1 && errno == ECHILD) {
        printf("所有子进程都已终止\n");
        break;
    }
}

四、进程程序替换

1、什么是程序替换?

一般情况,我们的程序只能执行我们的代码,但如果我们创建的子进程想要执行其他程序的代码,此时就需要通过程序替换来实现。即一个程序想执行其他程序的代码,这时就可以通过程序替换来实现。

2、测试代码,运用exec*系列的系统调用:

首先我们要知道,linux中每条指令都是一个程序,其次这里我们用到execl函数,函数原型如下:

参数解析:

(1)找到一个程序的前提是知道它的路径,所以第一个参数就是要替换执行程序的路径,可以是绝对路径(如/usr/bin/ls),也可以是相对路径(如./mybin)。

(2)第二个参数是一个可变参数列表,命令行中怎么写,参数就怎么传(例如"ls","-a","-l"),通常arg[0]是程序名字,后面是选项,最后必须以NULL结尾

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>


int main()
{
	printf("i am a process.mypid: %d\n",getpid());
	printf("exec begin....\n");
	execl("/usr/bin/ls","ls","-a","-l",NULL);
	printf("exec end....\n");
	return 0;
}

上面代码我用ls指令替换程序,并传入选项"-a","-l"的,当我运行程序时,就会执行ls命令

用top指令替换程序:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>


int main()
{
	printf("i am a process.mypid: %d\n",getpid());
	printf("exec begin....\n");
	//execl("/usr/bin/ls","ls","-a","-l",NULL);
	execl("/usr/bin/top","top",NULL);
	printf("exec end....\n");
	return 0;
}

3、几个细节

4、更改为多进程版本

首先为什么要更改为多进程版本?因为父进程可以得到执行的结果。

过程图:

因为创建子进程后,父子进程会执行同一份代码和数据,此时如果在子进程中进行程序替换,子进程原有的数据和代码就会被其他程序替换,如果不做处理,那么必定会影响父进程,这违背了进程的独立性,所以当子进程中进行程序替换时,会发生写实拷贝,从而不会影响父进程的代码和数据。

问题:这里我们就可以回答shell是如何运行一个指令的?

shell创建子进程,子进程去替换各种指令。

测试代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>


//多进程版本的程序替换
int main()
{
	pid_t id = fork();
	if(id == 0)
	{
		//child
		sleep(3);
		printf("i am child process: exec begin...\n");
		execl("/usr/bin/ls","ls","-a","-l",NULL);
		printf("exec end...\n");
		exit(1);
	}
	pid_t rid = waitpid(id,NULL,0);
	if(rid > 0)
	{
		printf("i am father: wait success\n");
	}
	return 0;
}

5、学习各种exec*接口

(1)execlp

该接口不用告诉系统,程序的路径是什么,只需要告诉程序叫什么名字,后面系统替换的时候,会自动去PATH环境变量默认的路径中去寻找。

所以第一个参数是程序名,第二个参数与execl一致。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>


//多进程版本的程序替换
int main()
{
	pid_t id = fork();
	if(id == 0)
	{
		//child
		printf("i am child process: exec begin...\n");
		//execl("/usr/bin/ls","ls","-a","-l",NULL);
		execlp("ls","ls","-l",NULL);
		printf("exec end...\n");
		exit(1);
	}
	pid_t rid = waitpid(id,NULL,0);
	if(rid > 0)
	{
		printf("i am father: wait success\n");
	}
	return 0;
}

我们可以看到参数:

这两个参数重复吗?答案是不重复的,第一个ls表示要替换的程序名称,第二个ls只是一个名称,即使和第一个替换程序名不一样也没有影响:

(2)execv

第一个参数也是程序的绝对路径或者相对路径

第二个参数,需要我们手动创建一个相同类型的数组进行传参,数组内容与execl第二个参数内容一致,如:

而我们所用的大多数指令(如ls)是C语言写的,所以就会有main函数,而main函数的第二个参数刚好是该数组类型,所以这里实际上我们是手动给main函数的命令行参数传参,同理main函数的第三个参数环境变量传参也是类似。

测试代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>


//多进程版本的程序替换
int main()
{
	pid_t id = fork();
	if(id == 0)
	{
		//child
		printf("i am child process: exec begin...\n");
		//execl("/usr/bin/ls","ls","-a","-l",NULL);
		//execlp("ls","HF","-l",NULL);
		char*const argv[] = {(char*)"ls",(char*)"-l",NULL};
		sleep(3);
		execv("/usr/bin/ls",argv);
		printf("exec end...\n");
		exit(1);
	}
	pid_t rid = waitpid(id,NULL,0);
	if(rid > 0)
	{
		printf("i am father: wait success\n");
	}
	return 0;
}

(3)、execve(唯一的系统调用)

通过man指令查看手册,发现execve单独在一个手册,这是为什么呐?

看到二号手册,我们就知道了,因为这些接口中,只有execve是系统调用,其他接口全是对execve的封装。

6、问题:exec*可以替换系统指令,那可以替换成我们自己的程序吗?

无论什么语言,只要能在liunx下面运行,就可以,因为所有语言程序运行后都变成了进程。

makefile同时形成两个可执行:

7、关于程序替换与环境变量的知识:

观察现象

我们自己写了一个程序:

在另一个程序中进行程序替换:

运行结果发现,当我们进行程序替换时,明明没有传递环境变量参数,但main函数的env参数依然有环境变量数据,原因在于父进程默认可以通过地址空间继承的方式,让所有子进程拿到环境变量,所以说,程序替换,不会替换环境变量数据。

如果我们putenv函数新增环境变量呐?现象是什么?

如果我们想设置全新的环境变量给子进程呐?(execle接口)

用execle接口:

该系统调用就有第三个参数envp,也是需要创建一个同类型数组:

这样传参相当于就是给main函数的第三个参数手动传参:

8、exec*系列接口结构图:


网站公告

今日签到

点亮在社区的每一天
去签到