1.进程创建

1.1fork函数

从已存在进程中创建新进程,新进程为子进程,原进程为父进程。

#include<unistd.h>
pid_t id=fork();
//返回值为,自进程中返回0,父进程返回子进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码。

内核会将分配新的内存块和内核数据结构给子进程。

将父进程的部分数据结构内容拷贝至子进程当中。

添加子进程到系统进度列表中。

fork返回时,开始调度器调度。

注意,fork之后谁先执行完全由调度器决定。

1.1.1fork返回值

子进程返回0,父进程返回的是子进程的pid。

1.1.2fork常规用法

一个父进程希望复制自己,同时执行不同的代码段。

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

//通过fork创建子进程并让父子进程运行不同的程序
int main()
{
	pid_t id=fork();
	if(id<0)
	{
		perror("fork");
		return 0;
	}
	else if(id==0)
    {
		while(1)
	    {
		    sleep(1);
		    printf("我是一个子进程,我的pid是:%d,我的父进程pid是:%d\n",getpid(),getppid());
		}
	}
	else
	{
		while(1)
		{	
			sleep(1);
			printf("我是一个父进程,我的pid是:%d,我的父进程pid是:%d\n",getpid(),getppid());
		}
	}
	return 0;
}

一个进程要执行不同的程序,如子进程从fork返回后,调用exec函数。

1.1.3 fork调用失败原因

系统中有太多的进程,导致内存资源吃紧无法重新为新进程分配内存资源等。

实际用户的进程数超过了限制。

2.写时拷贝

父进程创建的子进程,发生写时拷贝,会将父进程拷贝给子进程导致子进程会继承父进程的大部分属性信息,但是pid之类的肯定会被修改,由于父进程又指向着一份代码和数据,被拷贝过来的子进程也会指向同一份代码和数据

子进程没有自己的代码和数据,因为目前没有新的程序加载在内存里,所以此时子进程会默认共享父进程的代码和数据

写时拷贝,父子进程任何一方想要修改数据时,OS会把被修改的数据在底层拷贝一份供目标程序进行修改。

写时拷贝是一种延时申请技术,可以提高整机内存利用率。

3.进程终止

进程终止的本质就是释放系统资源,即释放进程申请的相关内核数据结构和对应的数据代码段。

3.1进程退出场景

代码运行完成,结果正确。

代码运行完成,结果不正确。

代码异常终止。

3.2进程常见退出方法

正常终止,可以通过指令echo $?查看进程退出码。

注意这个指令只能查看上一条进程运行后的退出码。

例如:

主函数结束,表示进程结束,会有一个返回值代表着程序的执行情况,而这个返回值就是退出码,不同的值表明不同的出错原因。

其他函数如自定义函数,只表示自己函数调用完成并返回。

而exit,_exit等函数则是在任何地方调用时,会结束进程并返回父进程,子进程指定的退出码。

异常退出,如ctrl+c,信号终止。

3.3退出码

退出码(退出状态)的作用是反馈命令执行结果:非零值代表执行失败;而程序因信号等异常终止时产生的退出码,并无有效含义。

linux shell中主要退出码:

0,命令执行成功

1,通用错误代码

2,命令/参数使用不当

126,权限被拒绝/无法执行

127,未找到命令,或不在环境变量PATH里

128+n,命令被信号从外部终止,或遇到致命错误

130,通过ctrl+c或者SIGNT终止(终止代码2或键盘中断)

143,通过SIGNT终止(默认终止)

225,退出码超过0-255范围,用退出取模重新计算

退出码1我们也可以解释为不被允许的操作。

130(对应 SIGINT 或 Ctrl+C)、143(对应 SIGTERM)是典型的终止信号退出码,遵循“128+n”规则,n是信号的终止编号,比如SIGINT编号为2(128+2=130),SIGTERM编号为15(128+15=143)。

可以使用strerror来获取对应退出码的信息描述

3.4exit函数

C/C++标准库中的进程终止函数,用于正常进程退出,并向bash返回退出状态码,是用户常用的退出接口。

#include<unistd.h>
void exit(int status);
//status定义了进程的终止状态,父进程通过wait获取status信息

并且进程如果exit退出的话,会进行缓冲区的刷新

而这里的缓冲区是库缓冲区,C语言所提供的,不是操作系统内部的缓冲区

#include<iostream>
using namespace std;

int main()
{
    cout<<"hello linux";
    exit(1);
    return 0;
}

输出为

3.5_exit函数

C/C++中的系统调用层函数,用于立刻终止当前进程,并由内核回收进程资源

#include<unistd.h>
void _exit(int status);
//status定义了进程的终止状态,父进程通过wait获取status

并且进程如果是_exit退出的话,不会进行缓冲区的刷新。

也可以这样说,exit函数是通过调用_exit函数间接实现的。

#include<iostream>
#include<unistd.h>
using namespace std;

int main()
{
    cout<<"hello linux";
    _exit(1);
    return 0;
}

输出为

3.6return退出

return退出是一种更常见的进程退出,执行return n相当于执行exit(n),因为调用主函数的运行时,会将主函数的返回值当做exit的参数。

4.进程等待

当子进程退出时,父进程对其不管,会造成僵尸进程,os无法主动清理僵尸进程占用的如PID资源等,从而导致内核资源泄漏问题。

进程一旦成为僵尸进程,便无法通过 kill 信号杀死,因为谁也不能杀死一个已经死掉的进程。

且自身无法主动退出,仅能通过父进程显式回收或父进程退出后由 init 接管回收,才能释放其占用的 PID 和内核元数据资源。

父进程是通过进程等待的方式,回收子进程的资源,子进程的退出信息-退出码。

4.1进程等待的方法

4.1.1wait方法

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int* status);

等待任意个退出的子进程,如果子进程没有退出,父进程将被堵塞在wait调用处。

成功返回等待进程的pid,失败则返回-1

int* status,输出型参数,获取子进程的退出情况,不关心则可以设置为NULL

总之,wait是用来回收僵尸进程和获取子进程的退出情况的。

案例:

//wait阻塞回收子进程,并获取退出情况
int main()
{
	pid_t id=fork();
	if(id==0)
	{
		//子进程正常退出
		cout<<"子进程,pid:"<<getpid()<<endl;
		sleep(2);
		exit(2);
	}
	else if(id>0)
	{
		cout<<"父进程等待子进程退出"<<endl;
		int status=0;
		pid_t ret=wait(&status);//回收子进程,并获取子进程的退出情况
		if(ret==-1)
		{
			perror("wait失败");
			exit(1);
		}
		if(WIFEXITED(status))
		{
			cout<<"回收子进程pid:"<<ret<<"正常退出,退出码:"<<WEXITSTATUS(status)<<endl;
		}
		else if(WIFSIGNALED(status))
		{
			cout<<"回收子进程PID:"<<ret<<"被信号终止,信号编号为:"<<WTERMSIG(status)<<endl;
		}
		else
		{
			perror("fork失败");
			exit(1);
		}
	return 0;
}

4.1.2waitpid方法

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid ,int* status,int options);

waitpid是wait的加强版,正常返回的是子进程的进程ID。

如果设置了选项WNOHANG,而调用中的waitpid发现没有已退出的子进程则返回0;

pid=-1时,等待任一个子进程,等效wait。

pid>0,等待其进程ID与pid相等的子进程。

status,输出型参数。

WIFEXITED,若为正常终止子进程返回的状态,则为真。

WEXITSTATUS,若WIFEXITED非0,则提取子进程的退出码。

options默认为0,表示阻塞等待。

options为WNOHANG时,非阻塞等待,若pid指定的子进程没有结束,则waitpid返回0,不予等待,若正常结束,则返回该进程的pid。

案例:

//使用waitpid指定回收pid,阻塞/非阻塞状态
int main()
{
	cout<<getppid()<<endl;
	pid_t id=fork();
	if(id==0)
	{
		cout<<"子进程正常退出,pid: "<<getpid()<<endl;
		sleep(1);
		exit(1);
	}
	pid_t rid=fork();
	if(rid==0)
	{
		cout<<"子进程2正常退出,pid: "<<getpid()<<endl;
		sleep(1);
		exit(2);
	}
	//父亲等待子进程2回收
	cout<<"等待回收子进程2"<<endl;
	int status=0;
	pid_t ret=waitpid(rid,&status,0);
	if(ret>0)
	cout<<"子进程2正常回收,pid: "<<rid<<"退出码为: "<<WEXITSTATUS(status)<<endl;
	ret=waitpid(id,&status,0);
	if(ret>0)
	cout<<"子进程1正常回收,pid: "<<id<<"退出码为: "<<WEXITSTATUS(status)<<endl;
	cout<<getppid()<<endl;
	return 0;
}

若子进程已经退出,调用wait/waitpid是,wait/waitpid会立即返回,释放资源,获取子进程退出信息。

若在任意时刻调用wait/waitpid,子进程存在且正常运行,则父进程可能堵塞。

若不存在该进程,则立即出错返回。

4.2获取子进程status

wait和waitpid有一个共同的参数status,该参数是一个输出型参数,由os填充,

若传递NULL,则表示不关心子进程的退出状态。

os会根据该参数,将子进程的退出状态反馈给父进程。

status不能简单的当做整型来看,以位图的角度来看。

4.2.1进程退出状态传递

子进程的退出信息存储,终止时,其退出码(exit_code),终止信号(exit_signal)会被保存在内核task_struct中。

父进程的获取方式,通过waitpid调用,从task_struct中读取这些信息到自身的status中,再通过WIFEXITED、WEXITSTATUS等宏解析

僵尸进程的产生,若父进程未调用wait/waitpid,子进程的task_struct会保留,仅释放用户态资源,形成僵尸进程,其存在的目的是保留退出信息供父进程获取。

4.3阻塞与非阻塞等待

阻塞等待会让父进程暂停执行,直到子进程退出。

非阻塞等待则让父进程在检查子进程状态后,可继续执行自身任务,无需暂停。

5.进程的程序替换

通过特定的接口,加载磁盘上的一个全新的程序,加载到调用进程的地址空间中。

5.1替换原理

用fork创建子进程后,子进程往往执行的是和父进程相同的程序,子进程需要调用exec函数执行另一个程序。

当进程调用exec函数时,该进程的用户空间代码和数据完全被新进程替换,从新进程启动开始,调用exec系列函数并不会创建新进程,可以查看调用前后进程id是否发生改变判断。

并且exec系列函数不用做返回值判断,一旦返回就是失败。

一旦程序替换成功就去执行新的代码了,原代码后半段已经不存在了。

//程序替换
int main()
{
	pid_t id=fork();
	if(id==0)
	{
		cout<<"我是子进程,pid: "<<getpid()<<endl;
		execl("./code","code",NULL);
		exit(19);
	}
	int status=0;
	pid_t rid=waitpid(id,&status,0);
	if(rid>0)
	{
		cout<<"正常退出"<<endl;
		if(WIFEXITED(status))
		cout<<"子进程pid: "<<id<<"退出码为: "<<WEXITSTATUS(status)<<endl;
	}
	return 0;
}

在上述代码中将子进程进行程序替换成当前目录下可运行程序code。

int main()
{
    cout<<"hello linux"<<endl<<"替换后pid: "<<getpid()<<endl;
    exit(1);
    return 0;
}

最终输出结果为

退出码不同,pid相同,成功的验证了程序替换成功,并且程序替换不会创建新的进程。

子进程进行程序替换也不会影响到父进程,因为进程之间的独立性以及写时拷贝。

execl能完成程序替换,是依赖底层加载器,作为系统调用接口,会触发加载器将目标程序的代码和数句加载到子进程的地址空间完成镜像替换,而这一操作是在子进程内部的独立操作。

5.2替换函数

一共有六种以exec开头的函数,同城为exec函数:

5.3函数解释

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

如果调用出错则返回-1

所以exec系列函数只有出错的返回,没有成功的返回。

5.4命名理解

putenv与exec系列函数,exec系列函数不修改父进程环境,仅为被替换程序提供独立环境,而putenv是直接修改当前进程的环境变量表,会影响自身与子进程继承。

execve是唯一的exec系列的系统调用,其余五个exec系列函数都是对execve的语言层封装。

其中

l,list,表示参数采用列表

v,vector,表示参数用数组

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

e,env,表示自己维护环境变量

execl,列表,不带路径,使用当前环境变量

execlp,列表,带路径,使用当前环境变量

execle,列表,不带路径,不使用当前环境变量,要自己组装

execv,数组,不带路径,使用当前环境变量

execvp,数组,带路径,使用当前环境变量

execvpe,数组,不带路径,不使用当前环境变量,要自己组装

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐