【Linux】进程控制

感谢阅读East-sunrise学习分享——进程控制
博主水平有限,如有差错,欢迎斧正感谢有你 码字不易,若有收获,期待你的点赞关注我们一起进步


在一个进程的生命周期中,有4个周期
1.进程创建
2.进程终止
3.进程等待
4.进程替换
进程的这4个周期都有其作用和意义,我们也可以对其进行控制
今天我们就以这4个周期为切入点来学习进程控制

目录

  • 一、进程创建
    • 1.再识fork函数
  • 二、进程终止
    • 1.main函数返回值
    • 2.查看程序退出码
    • 3.进程退出
      • 3.1进程退出的情况
      • 3.2进程常见退出方法
  • 三、进程等待
    • 1.进程等待必要性
    • 2.进程等待的方法
      • 2.1系统调用wait
      • 2.2系统调用waitpid
      • 2.3进程等待具体过程
    • 3.阻塞/非阻塞式等待
  • 四、进程程序替换
    • 1.替换原理
    • 2.替换函数
      • 1.认识exec函数
      • 2.exec函数的命名理解
    • 3.制作一个简易的shell

一、进程创建

1.再识fork函数

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

#include 
pid_t fork(void);

返回值:子进程中返回0,父进程返回子进程pid,出错返回-1

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

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

1.如何理解fork返回之后,给父进程返回子进程pid,给子进程返回0?

因为父进程:子进程 = 1:n (n>=1)

所以子进程不论如何都能找得到它的父进程,但是就需要给父进程返回各个子进程的id,便于父进程找到它的某个子进程


2.如何理解fork函数有两个返回值?

fork函数的核心操作内容便是创建进程,在fork函数之后,会有两个进程(两个执行流)彼此共同享用代码。而当一个函数准备return的时候,一般即是核心代码已经执行完毕。那么就意味着此时经过fork函数的调用后已经有了两个进程,才进行return。因此父进程和子进程各自执行return也就使得fork函数有两个返回值


3.如何理解同一个id值,会保存两个不同的值,让if else if同时执行?

这个问题经过我们之前对进程地址空间的学习后便十分容易了。调用fork函数的时候我们会用一个变量去接收其返回值pid_t id = fork( );而返回的本质就是写入,谁先返回,谁就先写入id。而因为进程具有独立性,因此在父子进程写入时是发生了写实拷贝。(具体情况和具体分析在之前博客已有涉及到)此时父子进程的id值不相同,但是打印出来的地址是相同的,因为打印出来的地址是虚拟地址。

fork常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数

fork调用失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

二、进程终止

1.main函数返回值

我们在平时编程时,main函数写完都会加上return 0;

int main()
{
	//...
	return 0;
}

return后面的值是代表进程退出的时候,对应的退出码。代码的执行结果正确与否我们可以通过标定各种退出码来检测。

而平时我们不关心代码的执行结果的错对,便是直接return 0;同样,如果我们关心代码的执行结果,可以根据不同情况设置不同的return+退出码。

#include 
int addToTarget(int from,int to)
{
     int sum = 0;
     for(int i = from; i < to ; i++)
     {
         sum += i;                                                                                                                                                                      
     }
     return sum;
 }
 
int main()
{
    int num = addToTarget(1,100);
    if(num == 5050)
        return 0;
    else
        return 1;
}

对于程序的退出码,系统是规定0表示成功,非0表示错误

2.查看程序退出码

但是计算机知道123对应的都是什么错误,我们人不知道,所以我们可以将错误码转化为错误信息打印出来

函数原型:

#include 
char* strerror(int errnum);

遍历打印错误码:

#include 
#include 
#include 
int main()
{
    for(int i = 0; i < 150; i++)
    {
        printf("num[%d]:%s\n",i,strerror(i));
    
    }
    return 0;                                                                                                                  
}

【Linux】进程控制_第1张图片

【Linux】进程控制_第2张图片

由此我们可以了解到系统定义的有意义的退出码一共是133个,每一个都有其对应的原因

查看进程的退出码

echo $?

$?会记录最近一个进程在命令行执行完毕时的退出码,供我们查看。

3.进程退出

3.1进程退出的情况

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果错误
  3. 代码未运行完毕,异常终止

3.2进程常见退出方法

正常终止(可以通过$?查看进程退出码)

  1. 从main返回(return)
  2. 调用exit
  3. 调用_exit

异常终止

  1. ctrl+c或由于其他异常(除0错误、野指针…)导致程序异常终止

库函数exit

函数原型:

#include 
void exit(int status);

库函数exit在进程退出后,会主动刷新缓冲区

系统调用_exit

函数原型:

#include 
void _exit(int status);

系统调用_exit在进程退出后,不会主动刷新缓冲区

#include 
#include 

int main()
{
    printf("hello world!\n");
    exit(20);
    //退出码可以自定义
    printf("hello world!!!!!\n");                                                                                                                                                  
    return 0;
}

在这里插入图片描述


三、进程等待

1.进程等待必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出
  • 父进程通过进程等待的方式,1.回收子进程资源 2.获取子进程退出信息

2.进程等待的方法

头文件:

#include 
#include 

2.1系统调用wait

函数原型:

pid_t wait(int* status);

返回值:等待成功返回被等待进程的pid,失败返回-1

参数:status是一个输出型参数,用于获取子进程的退出码和退出状态,不关心则可以设置为NULL

通过wait函数回收子进程资源:

#include 
#include 
#include 
#include 
#include 
 
int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork fail");
        exit(-1);
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt--)
        {
            printf("子进程:%d,父进程:%d   %d\n",getpid(),getppid(),cnt);
            sleep(1);
        }
        exit(0);//子进程退出
    }
    sleep(8);//让父进程暂时休眠
    pid_t ret = wait(NULL);//等待子进程,回收子进程资源
    if(id > 0)
    {
        //父进程
        printf("等待成功:%d\n",ret);
    }

    return 0;                                                                                                                                                                      
}

2.2系统调用waitpid

函数原型:

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

返回值

  • 当正常返回的时候waitpid返回收集到的子进程的进程id
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在

参数

  • pid:pid = -1,等待任意子进程,与wait等效;pid > 0,等待其进程id与pid相同的进程(指定具体进程)
  • status:WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出);WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
  • options:WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID

获取子进程status

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充;在进行函数调用时,我们需传入&status,这样才能取到其值
  • 如果传递NULL,表示不关心子进程的退出状态信息;否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位

【Linux】进程控制_第3张图片

status是一个32位的数,都是我们只关心它低16个比特位。status的次低8位(8-15)保存的是进程的退出状态,即是退出码;低7位(0~7)保存的是进程的终止信号

一个进程假如因为异常终止,操作系统会识别到这个进程有异常操作(比如除0、野指针、越界访问…)于是会给这个进程发信号,进程接收到这个终止信号后便会终止(代码没执行完,异常终止)

我们可以使用 kill -l 指令查看进程因为异常收到的各种信号原因

【Linux】进程控制_第4张图片

由此,我们根据status的值,便能将进程的所有退出情况反映出来

  • 正常终止 – 代码运行完毕,结果正确 ----> 终止信号为0(没有异常)、退出状态为0(运行结果正确)
  • 正常终止 – 代码运行完毕,结果错误 ----> 终止信号为0(没有异常)、退出状态为非0(运行结果错误)
  • 异常终止 – 代码未执行完毕,程序异常终止 ----> 终止信号非0(出现异常)、退出状态为0(退出状态位没有使用)

具体代码呈现

#include 
#include 
#include 
#include 
#include 

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork fail");
        exit(-1);
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt--)
        {
            printf("子进程:%d,父进程:%d   %d\n",getpid(),getppid(),cnt);
            sleep(1);
        }
        //exit(20);//子进程退出
    }
    int status = 0;
    pid_t ret = waitpid(id,&status,0);                                                                                                                                             
    if(id > 0)
    {
        //父进程
        printf("等待成功:%d,sig number:%d,child exit code:%d\n",ret,(status & 0x7F),(status>>8)&0xFF);
    }

    return 0;
}

【Linux】进程控制_第5张图片

进程的退出码我们也可以自定义设置

我们可以故意制造异常看看结果

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork fail");
        exit(-1);
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt--)
        {
            printf("子进程:%d,父进程:%d   %d\n",getpid(),getppid(),cnt);
            sleep(1);
            //野指针
            int* p = NULL;
            *p = 100;
        }
    }
    int status = 0;
    pid_t ret = waitpid(id,&status,0);                                                                                                                                             
    if(id > 0)
    {
        //父进程
        printf("等待成功:%d,sig number:%d,child exit code:%d\n",ret,(status & 0x7F),(status>>8)&0xFF);
    }

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c0qr9LT9-1674025070330)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230106161934883.png)]

11对应上图的异常信息码就是:段错误

2.3进程等待具体过程

  1. 子进程退出后变成僵尸状态,task_struct中的代码和数据会被释放掉,但是这时PCB还没能完全释放,因为此时里面存放着自己的退出信号、退出码
  2. wait/waitpid是系统调用,操作系统有资格也有能力去读取子进程的PCB
  3. 父进程通过进程等待的方式,获取子进程的退出信息,然后回收子进程资源(释放PCB)让子进程结束僵尸状态

3.阻塞/非阻塞式等待

阻塞式等待:当父进程调用wait/waitpid等待子进程时,第三个参数若为0,则是阻塞式等待所谓的阻塞式等待,如果子进程还未退出,父进程会暂停工作,一直等候着子进程

非阻塞式等待:当父进程调用waitpid等待子进程时,第三个参数若为WNOHANG,则是非阻塞式等待所谓的非阻塞式等待,如果父进程检测到子进程尚未退出,直接返回0,父进程不会原地不动地等他,而是继续执行自己的代码,进行自己的工作。如果使用while循环,便能实现非阻塞式等待轮询

非阻塞式等待不会占用父进程的时间,父进程可以在轮询的过程中做其他事情

#include 
#include 
#include 
#include 	
#include 
#include 

void OtherTask()
{
	printf("The child process is running , parent process is running other task\n");
}

int main()
{
	pid_t id = fork();
	assert(id != -1);

	if (id == 0)
	{
		//child
		int cnt = 5;
		while (cnt--)
		{
			printf("child process is running!\n");
			sleep(1);
		}
		exit(10);//子进程退出
	}
	int status = 0;
	//父进程对子进程进行轮询
	while (1)
	{
		pid_t ret = waitpid(id, &status, WNOHANG);
		if (ret == 0)//子进程尚未退出
		{
			printf("wait done, but child is running...parent running other things\n");
			OtherTask();
		}
		else if (ret > 0)//waitpid调用成功,子进程退出,返回值为被等待的子进程的id
		{
			printf("wait sucess,exit code:%d,sig:%d\n", (status >> 8) & 0xFF, status & 0x7F);
			break;//结束轮询
		}
		else//等于-1表示调用失败 (waitpid的第一个参数传入的是不存在的子进程id,则会失败)
		{
			printf("waitpid call fail\n");
			break;
		}
		sleep(1);
	}
	return 0;
}

四、进程程序替换

1.替换原理

在开始介绍进程程序替换之前,我们再回顾进程创建的目的

  1. 父进程希望复制自己,创建子进程;使父子进程同时执行同一个程序中不同的代码段(共享一个程序)
  2. 创建一个子进程去执行不同的程序

通过上文的介绍中,我们想实现第一个目的其实很简单,因为用fork创建进程,本就是以父进程为模板复制创建的而我们若要实现第二个目的,则需要实现对进程中程序的替换

通过之前对进程的铺垫学习,我们知道一个进程创建之后,对应的会在物理地址空间中载入其对应的代码和数据
⭕程序本质上就是磁盘中的可执行程序,因此程序替换的本质:将磁盘指定位置上的程序的代码和数据加载到内存中,覆盖进程本身的代码和数据,达到进程程序替换,使进程执行指定的程序

【Linux】进程控制_第6张图片

值得注意的是

  1. 进程具有独立性,所以当对子进程进行程序替换时,会发生写实拷贝
  2. 由于进程程序替换的本质是对进程原有的代码、数据进行覆盖,所以该进程原有的执行程序替换的后续代码也不会再执行,因为同样也被一并覆盖了

2.替换函数

1.认识exec函数

实现如上的操作,有一系列的替换函数能够实现

替换函数

#include 

//系统调用函数
int execve(const char *path, char *const argv[], char *const envp[]);
//由上面的系统调用再进行封装的函数
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 execl(const char *path, const char *arg, ...);

程序替换我们需要进行两步操作

  1. 找到程序(告诉系统要执行哪个程序)
  2. 介绍如何执行

如上的两步便从函数的两个参数得以实现

  • 第一个参数需要传入程序的地址
  • 第二个参数传入执行程序的方式(可理解为,你在命令行中怎么执行,就怎么传参)
  • 我们注意到参数中还有 … 这叫做可变参数列表;因为平时我们执行的时候会带上不定的选项,如:ls -a -i … 由此可以实现我们根据具体需求传入具体个数的参数,但是最后要以NULL结尾,表示已经传入完毕

举个栗子:父进程创建一个子进程去执行ls程序

#include 
#include 
#include 
#include 
#include 

int main()
{
    pid_t id = fork();
    
    if(id == 0)
    {
        //child
        execl("/user/bin/ls","ls","-l","-a","--color=auto",NULL);
        exit(-1);
	}
    
    wait(NULL);//父进程回收子进程
    return 0;
}

值得注意的是,exec*函数仅在发生错误替换失败时才返回-1;调用成功后是没有返回值的,因为exec一旦调用成功,后续代码将被覆盖,所以返回值也没什么意义了

2.exec函数的命名理解

exec函数在一个系统调用函数的基础上再封装出6个函数,为的就是适用于其他不同场景因此掌握exec其他函数命名的意义才能灵活调用

函数名带有 意义
l(list) 表示参数采用列表
v(vector) 以数组的形式传参
p(path) 操作系统会自动从环境变量PATH中搜索程序路径(我们可以直接传程序名)
e(environ) 可自定义环境变量

3.制作一个简易的shell

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define NUM 1024
#define OPT_NUM 64

char* lineCommand[NUM]

int main()
{
    while(1)
    {
        //输出提示符
        printf("用户名@主机名 当前路径# ");
        fflush(stdout);

        //获取用户输入
        char* s = fgets(lineCommand,sizeof(lineCommand)-1,stdin);
        assert(s != NULL);
        //清除最后一个\n
        lineCommand[strlen(lineCommand)-1] = 0;

        //"ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1-n 字符串切割
        char* myargv[OPT_NUM];
        myargv[0] = strtok(lineCommand," ");
        int i = 1;
        if(myargv[0] != NULL && strcmp(myargv[0],"ls") == 0)                                                                                                                         
        {
            myargv[i++] = (char*)"--color=auto";
        }
		//如果没有子串了,strtok -> NULL ,myargv[end] = NULL 
        while(myargv[i++] = strtok(NULL," "));

        //进程替换
        pif_t id = fork();
        if(id == 0)
        {
            //child
            execvp(myargv[0],myargv);
            exit(1);
        }

        wait(NULL);
     }
     return 0;
}

写在最后 我们今天的学习分享之旅就到此结束了
感谢能耐心地阅读到此
码字不易,感谢三连
关注博主,我们一起学习、一起进步

你可能感兴趣的:(Linux,linux)