【Linux】进程控制

创建进程[1~4]

1.写时拷贝

写时拷贝:父子代码共享,父子不写入时,数据也是共享的;当任意一方试图写入,就会以写时拷贝的方式各自一份副本。
写时拷贝是一种按需申请资源的策略!

2.fork函数

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

#include <unistd.h>
pid_t fork(void);
返回值:子进程返回0;父进程返回子进程id;出错时返回-1

当调用fork时,内核所做的有:
分配新的内存块与内核数据结构给子进程;
将父进程的部分内核数据结构拷贝给子进程;
将子进程添加到系统的进程列表中;
fork返回,开始调度;

3.fork创建子进程的目的(用法)

  1. 希望子进程会执行父进程的一部分代码;
  2. 希望子进程会执行一个全新的程序;

一个进程被fork创建出来,不是用命令行的方式加载到内存的,所以fork出来的子进程理论是没有代码和数据的,所以fork出来的子进程要么从父进程继承程序,要么就自己重新加载一个新的程序

4.fork失败的原因

  1. 系统中有太多的进程,资源不足,创建失败;
  2. OS会限制普通用户创建进程的数量,存在上限,太多会创建失败。

进程终止[5~7]

5.进程终止的情况

衡量进程的退出结果是通过:信号+退出码的方案。
在这里插入图片描述

编写代码的时候,在main函数的最后都会return 0,那么为什么要return 0呢?
——return 0;返回值0就是退出码,进程退出码用来表征在进程执行结束时,结果是否正确;
如果结果正确,则返回0;
如果结果错误,则返回非0;

int main()
{
	//...
	return 0;//进程退出码
}

当进程执行结果不正确,我们关心的是为什么不正确,所以结果错误时,返回值“非0”有很多(1, 2, 3…),通过不同的返回值,来表示不同的错误原因。

可以通过echo $?来获取退出码;
注:只会保留最近一次执行进程的退出码,所以首次运行echo $?的时候获取的是你自己进程的退出码,但是后续再次获取的就是上次echo $?的退出码了。

C提供的退出码获取

退出码可以自定义,同时C语言也提供了默认的退出码:

头文件:string.h
退出码获取接口:char *strerror(int errnum);

编写一个程序获取退出码:

#include <stdio.h>
#include <string.h>

int main()
{
    for(int i = 0; i < 200; i++)
    {
        printf("%d : %s\n", i, strerror(i));
    }
}

获取结果:
可以观察到C语言一共提供了133个退出码;
在这里插入图片描述
在这里插入图片描述
注意:不是所有程序都要遵守C语言的退出码,可以自己定义。

6.进程退出的理解

OS少了一个进程,OS就要释放进程对应的内核数据结构+代码和数据。

7.进程退出的方法

mian函数return退出

exit函数退出

头文件:unistd.h
函数:void exit(int status);
注:函数参数就是进程退出码,等价于main函数return x;

_exit函数退出

头文件:unistd.h
函数:void _exit(int status);
注:使用时看似与exit没有区别,但是实际不同,并且推荐使用exit。
区别:
exit会刷新缓冲区数据;
_exit不会做任何处理,直接退出;

例如:
下面的两个程序,第一个退出时会刷新缓冲区中的Hello world!打印到屏幕;第二个则不会打印直接退出。

int main()
{
    printf("Hello world!");
    sleep(1);
    exit(1);
}
----------------------------
int main()
{
    printf("Hello world!");
    sleep(1);
    _exit(1);
}

在这里插入图片描述

进程等待[8~10]

1.为什么要进程等待?
——回收子进程,避免内存泄漏;获取子进程执行的结果(退出信息);
(子进程的执行结果:退出码(代码正常执行结束) + 信号(代码运行异常))

2.什么是进程等待?
——通过系统调用,获取子进程退出码或退出信号+释放内存。

3.如何进程等待?
——wait/waitpid;

8.wait

作用:等待子进程状态的变化。(等子进程退出,回收子进程)

头文件:
#include<sys/types.h>
#include<sys/wait.h>
函数:
pid_t wait(int *status);
返回值:
成功:返回被等待进程pid,失败:返回-1
参数:
输出型参数,获取子进程退出状态(信号+退出码),若不关心子进程退出状态设置成为NULL

写一个测试程序:

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
		int cnt = 5;
		while(cnt)
		{
		    printf("这是子进程,子进程还没退出,还有%dS退出,pid:%d,ppid:%d\n", cnt--, getpid(), getppid());
		    sleep(1);
		}
		exit(0);
	    }
    //父进程
    sleep(10);
    pid_t ret_id = wait(NULL);
    printf("这是父进程,等待子进程结束,pid:%d,ppid:%d, ret_id %d\n", getpid(), getppid(), ret_id);
    sleep(5);
}

在这里插入图片描述

发现在子进程退出后,子进程进入僵尸状态(Z状态),等待父进程wait接收子进程,父进程接收后,子进程完全退出,父进程过一会也退出。

父子进程是同时推进的,那么父进程在wait等待子进程退出的这段时间里在干什么呢?
——父进程在这段时间内一直在等待子进程,子进程不退,父进程也不退。

9.waitpid

头文件:
#include<sys/types.h>
#include<sys/wait.h>
函数:
pid_ t waitpid(pid_t pid, int *status, int options);
参数:
pid:>0表示等待指定id的进程;==-1表示等待任意一个子进程(与wait等效)
status:输出型参数,获取子进程退出状态(信号+退出码),若不关心退出状态可以直接传NULL
返回值:
成功:返回被等待进程pid,失败:返回-1,还在等待:返回0

参数status(位图)

子进程的退出状态有两种:
退出码(代码正常执行结束) + 信号(代码运行异常)

  1. 代码正常执行结束:退出码,运行正确/不正确。
  2. 代码运行异常:信号

那么函数的第二个参数status,要如何存储两个数据(信号与退出码)呢?
——不要讲status看成一个完整的整数,而将其看做位图
在这里插入图片描述

所以可以按如下程序来测试获取进程的退出码和信号:

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
	    int cnt = 5;
	    while(cnt)
	    {
		    printf("这是子进程,子进程还没退出,还有%dS退出,pid:%d,ppid:%d\n", cnt--, getpid(), getppid());
		    sleep(1);
		}
		
		exit(100);
    }
    
    //父进程
    int status = 0;
    pid_t ret_id = waitpid(id, &status, 0);
    printf("这是父进程,等待子进程结束,pid:%d,ppid:%d, ret_id %d, child exit code:%d, child exit signal:%d\n", getpid(), getppid(), ret_id, (status>>8)&0xFF, status&0x7F);
}

结果:
在这里插入图片描述
发现获取的退出码等于100,与程序中子进程exit的一样,并且退出信号等于0,表示代码正常运行结束,但是结果不正确。(若将子进程exit改为0,则表示代码正常运行结束并且结果正确)

假如程序出现异常,则信号不等于0,具体退出信号可以在文章后面看。

解析:
(status>>8)&0xFF——获取status的次低8位;
status&0x7F——获取status的低7位;

从status中获取退出码的方法

退出码的获取:

  1. 可以通过上面的方式获取:(status>>8)&0xFF
  2. 可以通过宏获取:
    WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出
    WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

参数options

父进程在wait的时候,如果子进程没有退出,父进程在干什么?
——子进程没有退出的时候,父进程只能一直调用waitpid进行等待,这种等待称之为阻塞等待

如何理解阻塞等待?
——父进程在阻塞等待时肯定不是运行状态,那么就不在运行队列,那么此时父进程就在阻塞队列中(状态由R->S)。等子进程退出,再重新将父进程放回运行队列中去(状态变回R状态)。
在这里插入图片描述


如何让父进程在等待子进程期间不阻塞等待,去做其他的事呢?
——使用非阻塞等待,waitpid默认的方式是阻塞等待。

通过修改第三个参数可以改变等待的策略:

options的参数:

  1. 0:阻塞等待
  2. WNOHANG:非阻塞等待

下面写一段程序来测试一下非阻塞等待的效果:

int main()
{
    pid_t id = fork();
    
    if(id == 0)
    {
		int cnt = 5;
		
		while(cnt)
		{
		    printf("这是子进程,子进程还没退出,还有%dS退出,pid:%d,ppid:%d\n", cnt--, getpid(), getppid());
		    sleep(1);
		}
		
		exit(100);
    }
    
    //父进程
    while(1)
    {
	    int status = 0;
	    pid_t ret_id = waitpid(id, &status, WNOHANG);
	    
		if(ret_id < 0)
		{
		    printf("waitpid error!\n");
		    exit(1);
		}
		else if(ret_id == 0)
		{
		    printf("子进程还未退出,父进程先去干别的\n");
		    sleep(1);//表示父进程去干别的了
		    continue;
		}
		else
		{
	        printf("这是父进程,等待子进程结束,pid:%d,ppid:%d, ret_id %d, child exit code:%d, child exit signal:%d\n", getpid(), getppid(), ret_id, (status>>8)&0xFF, status&0x7F);
		    break;
		}
    }
}

运行结果:
在这里插入图片描述

理解:
非阻塞等待就是父进程每过一段时间定期询问一次子进程是否退出,如果退出了那就接收,如果没退出那么就去执行别的程序。

而阻塞等待就是父进程直接停下来等待子进程退出,不会自己去执行别的程序。


简单非阻塞等待的程序学习例子:

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

#define TASK_NUM 10//任务个数

//模拟预设一批任务
void sync_disk(){
	printf("这是刷新数据的任务!\n");
}
void sync_log(){
	printf("这是同步数据的任务!\n");
}
void net_send(){
	printf("这是网络发送的任务!\n");
}

//函数指针
typedef void (*func_t)();//typedef void (*func_t)(); == typedef void (*)() func_t;

//任务列表
func_t other_task[TASK_NUM] = {NULL};

//载入任务
int LoadTask(func_t func)
{
	int i = 0;
	for(i; i < TASK_NUM; i++)
	{
		if(other_task[i] == NULL)
		{
			break;
		}
	}

	if(i == TASK_NUM)//任务加载满了
	{
		return -1;
	}
	else//可以加载任务
	{
		other_task[i] = func;
	}

    return 0;
}

//初始化任务
void InitTask()
{
	int i;
	for(i = 0; i < TASK_NUM; i++)
	{
		other_task[i] = NULL;
	}
	//载入三个任务
	LoadTask(sync_disk);
	LoadTask(sync_log);
	LoadTask(net_send);
}

//运行任务
void RunTask()
{
	int i;
	for(i = 0; i < TASK_NUM; i++)
	{
		if(other_task[i] == NULL)
		{
			continue;
		}
		other_task[i]();
	}
}

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
		//子进程
		int cnt = 5;
		while(cnt)
		{
	    	printf("这是子进程,子进程还没退出,还有%dS退出,pid:%d,ppid:%d\n", cnt--, getpid(), getppid());
	    	sleep(1);
		}
		exit(100);
    }
    //父进程

	InitTask();

    while(1)
    {
        int status = 0;
        pid_t ret_id = waitpid(id, &status, WNOHANG);
		if(ret_id < 0)
		{
	    	printf("waitpid error!\n");
	    	exit(1);
		}
		else if(ret_id == 0)
		{
	    	//子进程还未退出,父进程先去干别的
			RunTask();
	    	sleep(1);
	    	continue;
		}
		else
		{
			if(WIFEXITED(status))
			{
				printf("wait success, child exit code : %d\n", WEXITSTATUS(status));
			}
			else
			{
				printf("wait success, chile exit signal : %d\n", status&0x7F);
			}
        	//printf("这是父进程,等待子进程结束,pid:%d,ppid:%d, ret_id %d, child exit code:%d, child exit signal:%d\n", getpid(), getppid(), ret_id, (status>>8)&0xFF, status&0x7F);
	    	break;
		}
    }
}

10.系统提供的信号

查看系统提供的信号:kill -l
有如下信号:1~31为普通信号
在这里插入图片描述
当程序中有错误自己崩溃的时候会根据不同的错误返回不同的信号(野指针…);
如果用kill -9来手动杀一个进程,则进程的退出信号为9;


父进程是如何获取子进程的退出信息的?
——在描述进程的内核数据结构task_struct(pcb)中存在两个整数:int exit_code;int exit_signal;
当子进程在执行完毕的时候,会将main函数的返回值写到exit_code中;
如果出现了异常,OS会把进程异常时遇到的信号的编号写到exit_signal中;
OS会将pcb维护起来(因为在OS内核当中),我们要用waitpid这样的系统调用接口,它们会在进程列表当中找到要等待的目标进程(根据pcb中存的子进程pid找到),然后按照前面说的位图格式,把exit_code和exit_signal设置进入waitpid的第二个参数status中,所以用户就取到了子进程的这两个特殊信息。


学习的命令

获取退出码:echo $?
注:只会保留最近一次执行进程的退出码。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值