Linux:进程控制

我们之前已经学过fork函数的应用了(从已存在进程中创建一个新进程:新进程为子进程,原进程为父进程)

接下来我们来研究一下fork函数的运行的内核是怎样的

fork()详解

fork是复制进程的函数,程序一开始就会产生一个进程(这个进程为父进程)当它执行到fork时,fork就会复制一份原来的进程,创建一个新进程(就是子进程)。他们一起向下执行父进程剩余的代码。

fork()函数在执行时:

分一块新的PCB内存块和内核数据给子进程->将父进程的数据和代码copy给子进程->将子进程的添加到父进程执行的程序列表中->fork开始返回,调度器开始调度

进程=内核的相关管理数据结构(task_struct + mm_struct + 页表)+ 代码和数据

已知fork函数的返回值是这样的:

子进程返回0

父进程返回子进程的pid

子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

进程具有独立性就在于子进程代码数据和父进程共享,但因为写时拷贝又不影响父进程

  1 #include<stdlib.h>                                                           
  2 #include <unistd.h>
  3 #include<stdio.h>
  4 int main(void)
  5 {
  6   pid_t pid;
  7 
  8   printf("Before: pid is %d\n", getpid());
  9   if ((pid = fork()) == -1)
 10     perror("fork()"), exit(1);
 11   printf("After:pid is %d, fork return %d\n", getpid(), pid);
 12   sleep(1);
 13   return 0;
 14 }
~

fork()的常规用法

常规用法:一个父进程希望能创造一个子进程复制自己的代码和数据,使父子进程同时执行不同的代码段(父进程等待客户端的请求,生成子进程来处理请求)

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

进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行(当前进程剩下的程序代码便不再执行)。调用 exec 并不创建新进程,所以调用 exec 前后该进程的id并未改变。

fork调用失败原因

系统中有太多的进程:实际用户的进程数超过了限制导致调用失败

进程终止

进程终止是指释放曾经的代码和数据所占据的空间,也是在释放内核数据结构(task_struct,当进程状态是Z就要释放对应PCB)

终止的三种情况

来看两段代码

#include<stdio.h>
#include<unistd.h>
 
int main()
{
	printf("hello world!\n");
	return 0;
}
#include<stdio.h>
#include<unistd.h>
 
int main()
{
	printf("hello world!\n");
	return 100;
}

可以看到他们只有返回值不同

($) 是 bash 脚本中的通用特殊字符,用于变量和命令替换、表示特殊变量以及用作转义字符。

Shell特殊变量讲解:$n、$#、$*、$@、$?、$$各自代表的意思及用法-CSDN博客

$在这里我们使用他的一个用法:$?

?:父进程bash获取到的最近一个子进程的退出码(0就是成功,非0就是失败)

来我们使用一下:

如此我们可以看出退出码的意义:告诉关心方(父进程在这里就是关心方)任务完成的怎么样

因为成功的退出码就是0,而!0有很多,所以不同!0值一方面表示失败,一方面还表示失败的原因(我们可以规定不同的返回值对应出不同的错误)

像这样:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
 
int main()
{
	int errcode = 0;
	for (errcode = 0; errcode <= 255; errcode++)
	{
		printf("%d:%s\n", errcode, strerror(errcode));
	}
	return 0;
}

C 库函数 char *strerror(int errnum) 从内部数组中搜索错误号 errnum,并返回一个指向错误消息字符串的指针。strerror 生成的错误字符串取决于开发平台和编译器。

这样就可以通过数字打印不同的错误信息了

父进程知道子进程的退出码,就可以知道子进程的退出情况。正常退出了吗?错误了吗?错哪了?(末日三问)

错误码也可以自己设定:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int Div(int x, int y)
{
	if (0 == y)
	{
		return -1;
	}
	else
	{
		return x / y;
	}
}
int main()
{
    printf("%d\n",Div(-1,1));
	return 0;
}

但是这样没法判断是y==0导致返回错误码-1,还是本来的计算结果就是-1

所以可以这样改:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
 
//自定义枚举常量
enum
{
	Success = 0,
	Div_Zero,//打印出来相当于1
	Mod_Zero,//打印出来相当于2
};
 
int exit_code = Success;
 
int Div(int x, int y)
{
	if (0 == y)
	{
		exit_code = Div_Zero;
		return -1;
	}
	else
	{
		return x / y;
	}
}
int main()
{
	printf("%d\n", Div(-1, 1));
	return exit_code;
}

enum 关键字:后面跟着枚举类型的名称,以及用大括号 {} 括起来的一组枚举常量。每个枚举常量可以用一个标识符来表示,也可以为它们指定一个整数值,如果没有指定,那么默认从 0 开始递增。

还可以再加一个接口

接口规定了模块做什么,仅规定应用程序可能使用的那些标识符,而尽可能隐藏不相关的表示细节和算法。 在C语言中,接口通过一个头文件指定,头文件的扩展名通常为.h 。

#include<stdio.h>
#include<unistd.h>
#include<string.h>
 
//自定义枚举常量
enum
{
	Success = 0,
	Div_Zero,
	Mod_Zero,
};
 
int exit_code = Success;
 
const char* CodeToErrString(int code)
{
	switch (code)
	{
		case Success:
			return "Success";
		case Div_Zero:
			return "div zero!";
		case Mod_Zero:
			return "mod zero!";
		default:
			return "unknow error!";
	}
}
 
int Div(int x, int y)
{
	if (0 == y)
	{
		exit_code = Div_Zero;
		return -1;
	}
	else
	{
		return x / y;
	}
}
int main()
{
	printf("%d\n", Div(-1, 1));
	printf("%s\n", CodeToErrString(exit_code));
	return exit_code;
}

进程终止,也有很多种情况;就好像我不想上学也有很多原因一样

1.代码跑完,结果正确

2.代码跑完,结果不正确(正确与否可通过进程退出码决定)

3.代码执行时,出现了异常,提前退出了(系统&&自定义退出码)

什么叫进程崩溃?

编译运行的时候,操作系统发现你的进程做了不该做的是事,于是OS杀掉了你的进程,就叫崩溃

那都异常了,我们的退出码还有什么意义?没有意义啊,运行完报出的错误码才有用,运行都没完那退出码就自然没有意义了

进程出现了异常,本质是因为进程收到了OS发给进程的信号

一份普通的代码:

#include<stdio.h>
#include<unistd.h>
int main()
{
	while (1)
	{
		printf("I am a process,pid:%d\n", getpid());
	}
	return 0;
}

这是一个正常进程捏,因为while循环,所以一直运行

我们可以用命令来停止它

kill -9 pid

由于进程收到了OS发给进程的信号,所以进程不得不终止 

我们再创建一个错误的进程:

#include<stdio.h>
#include<unistd.h>
int main()
{
	int* p = NULL;
	while (1)
	{
		printf("I am a process,pid:%d\n", getpid());
		sleep(1);
		*p = 100; //p应该是个地址,所以这里是故意的     
	}
	return 0;
}

在Linux中运行这段代码会发现出现段错误:Segmentation fault,这是一种退出信号:

OS提前终止进程  

我们通过观察进程退出的时候退出信号是多少就可以判断我们的进程为何异常了

判断流程:

1.先确认是否异常

2.不是异常就是代码跑完了,直接看退出码

衡量一个进程退出,只需要两个数字:退出码退出信号

进程退出时会把退出码和退出信号写入PCB(方便父进程知道)

进程的终止有几种方式呢?

我们刚刚发现了,main函数的返回就可以终止进程,还有什么方法呢?

代码调用exit函数(头文件为stdlib.h)

exit(0);        //里面数字是return数

exit()相似的另一个函数是_exit()

他们的区别是:_exit()在退出时,是不会冲刷缓冲区的

你写一个printf("hello world"),如果用exit()这个没有被\n冲刷的printf()就会被exit()强制冲刷

但是_exit()则会留下你的缓冲区

exit()的运行过程其实就是:调用_exit()+清理缓存

1. 执行用户通过 atexit或on_exit定义的清理函数

2. 关闭所有打开的流,所有的缓存数据均被写入

3. 调用_exit

进程等待

什么是等待?进程为什么要等待?

任何子进程在退出的情况下,一般必须要被父进程进行等待 

如果进程在退出时,父进程不管他的儿子,自己先退出,那子进程就会变成Z状态(僵尸状态),发生内存泄露。内存一旦变成僵尸状态,连kill都停不了他,因为谁也没有办法杀死一个已经死去的进程

所以父进程为什么等待:

1.父进程通过等待,解决子进程退出的僵尸问题,回收系统资源(一定要考虑的)

2.获取子进程的退出信息知道子进程是什么原因退出的(可选功能)

怎么等待呢?

使用两个函数:wait、waitpid

wait:

wait的返回值:等待成功时,返回子进程的pid

wait的参数:等待任意一个子进程退出(获取子进程的退出状态,不关心可以不加参数(也就是置NULL))

pid_t wait(int* status);

来看一看回收的过程:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
 
void ChildRun()
{
	int cnt = 5;
	while (cnt)
	{
		printf("I am child process,pid:%d,ppid:%d\n", getpid(), getppid());
		sleep(1);
		cnt--;
	}
}
 
int main()
{
	printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());
 
	pid_t id = fork();
	if (id == 0)
	{
		ChildRun();
		printf("child quit ...\n");
		exit(0);
	}
	sleep(3);
	pid_t rid = wait(NULL);
	if (rid > 0)
	{
		printf("wait success,rid:%d\n", rid);
	}
	sleep(3);
	printf("father quit\n");
	return 0;
}

可以看到子进程在被父进程回收前是处于僵尸状态的: 

 父进程在等待时候也没干其他事,只是等

如果子进程没有退出,父进程其实一直在进行阻塞等待

什么叫阻塞等待?

来看看waitpid():

#include <sys/types.h> 
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);

参数值说明

pid_t pid
pid<-1    等待进程组号为pid绝对值的任何子进程。
pid=-1    等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数。
pid=0    等待进程组号与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。
pid>0    等待进程号为pid的子进程。

int *status

这个参数将保存子进程的状态信息,有了这个信息父进程就可以了解子进程为什么会推出,是正常推出还是出了什么错误。如果status不是空指针,则状态信息将被写入

一些非常有用的宏来帮助解析这个状态信息,这些宏都定义在sys/wait.h头文件中。主要有以下几个:

WIFEXITED(status) 如果子进程正常结束,它就返回真;否则返回假。
 WEXITSTATUS(status)如果WIFEXITED(status)为真,则可以用该宏取得子进程exit()返回的结束代码
WIFSIGNALED(status)如果子进程因为一个未捕获的信号而终止,它就返回真;否则返回假。
WTERMSIG(status)如果WIFSIGNALED(status)为真,则可以用该宏获得导致子进程终止的信号代码。
WIFSTOPPED(status)

如果当前子进程被暂停了,则返回真;否则返回假。

WSTOPSIG(status)如果WIFSTOPPED(status)为真,则可以使用该宏获得导致子进程暂停的信号代码。

int options

参数options提供了一些另外的选项来控制waitpid()函数的行为。如果不想使用这些选项,则可以把这个参数设为0。

WNOHANG如果pid指定的子进程没有结束,则waitpid()函数立即返回0,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号。
WUNTRACED如果子进程进入暂停状态,则马上返回。

返回值

当正常返回的时候waitpid返回收集到的子进程的pid(等待成功,紫禁城退出

父进程回收成功)

若返回值为0,那证明检测成功,但紫禁城并未退出,需要再次进行等待

若设置了选项WNOHANG,调用中waitpid发现没有已退出的子进程可收集,则返回0        

如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在

pid_t rid = waitpid(-1, NULL, 0);

这就是等待任意一个进程的意思捏:

Status的用法

退出信息就是退出码和退出信号,可是Status(也就是wait()的参数)只有一个数,可以当作位图看待:

       

常用宏函数分为日如下几组(上面的表格我们已经指出这个宏函数了):
1、 WIFEXITED(status) 若此值为非0 表明进程正常结束。
若上宏为真,此时可通过WEXITSTATUS(status)获取进程退出状态(exit时参数)
示例:

     if(WIFEXITED(status)){
            printf("退出值为 %d\n", WEXITSTATUS(status));
        }

2、 WIFSIGNALED(status)为非0 表明进程异常终止。
若上宏为真,此时可通过WTERMSIG(status)获取使得进程退出的信号编号
用法示例:

    if(WIFSIGNALED(status)){
        printf("使得进程终止的信号编号: %d\n",WTERMSIG(status));   
    }

3、 WIFSTOPPED(status)为非0 表明进程处于暂停状态
若上宏为真,此时可通过WSTOPSIG(status)获取使得进程暂停的信号编号
4、 WIFCONTINUED(status) 非0表示暂停后已经继续运行。

如果子进程死循环了怎么办?

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
 
void ChildRun()
{
	int cnt = 5;
	while (1)
	{
		printf("I am child process,pid:%d,ppid:%d\n", getpid(), getppid());
		sleep(1);
		cnt--;
	}
}
 
int main()
{
	printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());
 
	pid_t id = fork();
	if (id == 0)
	{
		ChildRun();
		printf("child quit ...\n");
		exit(123);
	}
	sleep(3);
	int status = 0;
	pid_t rid = waitpid(id, &status , 0);
	if (rid > 0)
	{
		printf("wait success,rid:%d\n", rid);
	}
	else
	{
		printf("wait failed\n");
	}
	sleep(3);
	printf("father quit,status:%d,child quit code:%d,child qiut signal:%d\n",status,(status>>8)&0xFF, status & 0x7F);
	return 0;
}

父进程就会一直等。。

如果子进程异常呢?

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
 
void ChildRun()
{
	int cnt = 5;
	int* p = NULL;
	while (cnt)
	{
		printf("I am child process,pid:%d,ppid:%d\n", getpid(), getppid());
		sleep(1);
		cnt--;
	}
	*p = 10;
}
 
int main()
{
	printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());
 
	pid_t id = fork();
	if (id == 0)
	{
		ChildRun();
		printf("child quit ...\n");
		exit(123);
	}
	sleep(3);
	int status = 0;
	pid_t rid = waitpid(id, &status , 0);
	if (rid > 0)
	{
		printf("wait success,rid:%d\n", rid);
	}
	else
	{
		printf("wait failed\n");
	}
	sleep(3);
	printf("father quit,status:%d,child quit code:%d,child qiut signal:%d\n",status,(status>>8)&0xFF, status & 0x7F);
	return 0;
}

可以看见提示说你的进程异常了,代码有问题

如果子进程没有退出,父进程在进行执行waitpid进行等待(等待某种条件发生,只不过如今的条件恰好是紫禁城退出),阻塞等待(进程阻塞,父进程什么事都没干)

但是现在我们使用的大部分进程都是阻塞等待的,WNOHANG选项(上文提到的参数:option)就是非阻塞等待,如果一直挂着什么都做不了,我们把这种情况叫做服务器宕机

什么是非阻塞等待?

当一个任务不需要等待操作的完成时,该任务会继续执行其他操作,而不是停止等待,就是非阻塞等待

你怎么知道我们是不是非阻塞等待?我们必须得边判断子进程是不是在阻塞,边执行其他进程啊

所以当我们我们采用非阻塞等待的时候,一般要加上循环,直到检测到紫禁城退出,我们把这种方案叫做非阻塞轮询方案

而阻塞等待优点也很明显了,就是简单可靠,但非阻塞时父进程可以做其他的事

看看非阻塞轮询的代码

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<unistd.h>
 
void ChildRun()
{
	int cnt = 5;
	while (cnt)
	{
		printf("I am child process,pid:%d,ppid:%d,cnt:%d\n", getpid(), getppid(), cnt);
		cnt--;
		sleep(1);
	}
}
 
int main()
{
	printf("I am father process,pid:%d,pid:%d\n", getpid(), getppid());
 
	pid_t id = fork();
	if (id == 0)
	{
		ChildRun();
		printf("child quit\n");
		exit(123);
	}
	while (1)
	{
		int status = 0;
		pid_t rid = waitpid(id, &status, WNOHANG);	//进行非阻塞等待,一直查询子进程是不是在阻塞
		if (rid == 0)
		{
			printf("child is running, father check next time!\n");
			//DoOtherThing();
		}
		else if (rid > 0)
		{
			if (WIFEXITED(status))
			{
				printf("child quit success,child exit code:%d\n", WEXITSTATUS(status));
			}
			else
			{
				printf("child quit unnormal!\n");
			}
			break;
		}
		else
		{
			printf("waitpid failed!\n");
			break;
		}
	}
	return 0;
}

刚说在父进程等待的时候还可以做其他事,下面来举个栗子:基于函数指针级别的对父进程完成任务进行解耦(对父进程下的子进程进行非阻塞等待)

process.c

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<unistd.h>
#include"task.h"
 
typedef void(*func_t)();
 
#define N 3
func_t tasks[N] = { NULL };
 
void LoadTask()
{
	tasks[0] = Printlog;
	tasks[1] = Download;
	tasks[2] = MysqlDataSync;
}
 
void HanderTask()
{
	for (int i = 0; i < N; i++)
	{
		tasks[i]();
	}
}
 
void DoOtherThing()
{
	HanderTask();
}
 
void ChildRun()
{
	int cnt = 5;
	while (cnt)
	{
		printf("I am child process,pid:%d,ppid:%d,cnt:%d\n", getpid(), getppid(), cnt);
		cnt--;
		sleep(1);
	}
}
 
int main()
{
	printf("I am father process,pid:%d,pid:%d\n", getpid(), getppid());
 
	pid_t id = fork();
	if (id == 0)
	{
		ChildRun();
		printf("child quit\n");
		exit(123);
	}
	LoadTask();
	while (1)
	{
		int status = 0;
		pid_t rid = waitpid(id, &status, WNOHANG);	//进行非阻塞等待
		if (rid == 0)
		{
			printf("child is running, father check next time!\n");
			//DoOtherThing();
		}
		else if (rid > 0)
		{
			if (WIFEXITED(status))
			{
				printf("child quit success,child exit code:%d\n", WEXITSTATUS(status));
			}
			else
			{
				printf("child quit unnormal!\n");
			}
			break;
		}
		else
		{
			printf("waitpid failed!\n");
			break;
		}
	}
	return 0;
}

task.h

#pragma once
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<unistd.h>
 
void Printlog();
void Download();
void MysqlDataSync();

task.c

#include"task.h"
 
void Printlog()
{
	printf("begin Printlog...\n");
}
 
void Download()
{
	printf("begin Download...\n");
}
 
void MysqlDataSync()
{
	printf("begin MysqlDataSync...\n");
}

makefile

myprocess:myprocess.c task.c
gcc - o $@ $ ^
.PHONT:clean
clean:
	rm -f myprocess

啊啊啊啊好累,睡醒了交到github上

  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值