【Linux】进程控制

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

目录

前言

一、进程创建

fork函数初识

fork 函数返回值

创建进程的时候,是先有内核的数据结构呢?还是先有进程的代码和数据呢?

写时拷贝

fork常规用法

fork调用失败的原因

二、进程终止

终止是在做什么吗

进程终止的三种情况

查看整个系统提供错误码的编号以及对应的含义

我们自定义描述一下退出码的信息

如何终止异常

三、进程等待

进程等待必要性

进程等待的方法

wait方法

waitpid方法

获取子进程status

进程的阻塞等待

进程的非阻塞等待(等待时,允许父进程做一些事情)

总结



前言

世上有两种耀眼的光芒,一种是正在升起的太阳,一种是正在努力学习编程的你!一个爱学编程的人。各位看官,我衷心的希望这篇博客能对你们有所帮助,同时也希望各位看官能对我的文章给与点评,希望我们能够携手共同促进进步,在编程的道路上越走越远!


提示:以下是本篇文章正文内容,下面案例可供参考

一、进程创建

fork函数初识

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

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

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

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

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。

int main(void)
{
	pid_t pid;

	printf("Before: pid is %d\n", getpid());
	pid_t rid = fork();
	if (rid == -1)
	{
		perror("fork()"),
		exit(1);
	}
	
	printf("After:pid is %d, fork return %d\n", getpid(), pid);
	sleep(1);
	return 0;
}

运行结果:

[root@localhost linux]# ./a.out

Before: pid is 43676

After:pid is 43676, fork return 43677

After:pid is 43677, fork return 0

这里可以看到,Before只输出了一次,而After输出了两次。其中,Before是由父进程打印的,而调用fork函数之后打印的两个After,则分别由父进程和子进程两个进程执行。也就是说,fork之前父进程独立执行,而fork之后父子两个执行流分别执行。

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

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

task_struct:进程控制块(描述进程)

mm_struct:进程地址空间(虚拟地址)

fork 函数返回值

  • 子进程返回0
  • 父进程返回的是子进程的pid

fork()函数为什么会返回两个值?
fork()函数最终return的时候,创建子进程的核心数据已经做完了,也就是说在执行fork()函数最终的return时,子进程和父进程都会被调度,return也是语句,往后的代码都会被共享,所以return被执行了两次。
return的本质就是对进程当中进行写入。

为什么给父进程返回的是子进程的PID,给子进程返回的是0?
给子进程返回的是0,是因为想要知道子进程有没有被创建成功,通过0来返回得知结果。
我们为了让父进程方便对子进程进行标识,进而进行管理。

创建进程的时候,是先有内核的数据结构呢?还是先有进程的代码和数据呢?

你高考完之后,考上报的大学了,那么你的方案袋会提前被录取你的学校拿走,所以,
你还没有报道的时候,你已经是这个学校的学生了,这就是新建进程;当你人去了学校以后,才是代码和数据。
同理:创建一个进程的时候,先创建内核的相关管理数据结构,再有进程的代码和数据。

命令行中启动的所有进程,都是bash的子进程。

写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副 本。具体见下图:

fork常规用法

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

fork调用失败的原因

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

二、进程终止

终止是在做什么吗

  • 1、释放曾经的代码和数据所占据的空间
  • 2、释放内核数据结构

进程终止的三种情况

a、代码跑完,结果正确

b、代码跑完,结果不正确(正确与否:可以通过进程的退出码决定!)

c、代码执行时,出现了异常,提前退出了。

VS编程运行的时候,崩溃了 --- OS发现你的进程做了不该做的事情,OS杀了进程。一旦出现异常,退出码就没有意义了。

为什么进程会出异常?因为进程收到了OS发给进程的信号!

kill -11 pid   // 11:SIGSEGV

我们可以看进程退出的时候,退出信号是多少,就可以判断我的进程为什么异常了!!!

我们看下面这一段代码:

#include <stdio.h>
#include <unistd.h> 
int main()
{
    printf("I am process, pid: %d, ppid: %d\n", getpid(), getppid());
    sleep(2);
    return 100;
 }
echo $?  
//能打印出100

在shell当中存在一个变量,这个变量名是

echo 是内建命令,只会打印bash本身内部的变量数据
?:父进程bash获取到的,最近一个子进程退出的退出码(内建命令虽然没有创建子进程,但是是由bash直接执行的,所以也会修改退出码),退出码告诉关心方(父进程),我把任务完成得怎么样了!
退出码0:表示成功      退出码!0:表示失败

查看整个系统提供错误码的编号以及对应的含义

man strerror  
//把一个错误码转化成所对应错误信息字符串的描述
#include <stdio.h>
#include <unistd.h> 
#include <string.h>
int main()
{
  // 查看整个系统提供错误码的编号以及对应的含义
	for (int errcode = 0; errcode <= 255; errcode++)
	{
		printf("%d: %s\n", errcode, strerror(errcode));
	}
    printf("I am process, pid: %d, ppid: %d\n", getpid(), getppid());
    sleep(2);
    return 100;
 }

我们自定义描述一下退出码的信息

// 退出码可以使用默认,也可以使用自定义
// 我们自定义描述一下退出码的信息
// 自定义枚举常量
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()
{
	int result = Div(10, 100);
	printf("result: %d [%s]\n", result, CodeToErrString(exit_code));
	result = Div(10, 0);
	printf("result: %d [%s]\n", result, CodeToErrString(exit_code));
	return exit_code;
}

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

我们子进程的退出码或退出信号一定要让我们的父进程(bash)知道,子进程执行完自己的代码和数据,最后return或者异常,我们的子进程的task_struct结构体里面有许多属性,包括退出信息编号、退出码等;子进程退出时,会先将自己的代码和数据释放掉,但不能把自己的对应的PCB释放掉,要把自己的PCB维持一段状态,变成Z状态,让父进程来读取;当一个进程退出时,他会把进程执行的最终的退出信号或者退出码写入到子进程的PCB当中。

如何终止异常

a、main函数return,表示进程终止(非main函数,return,函数结束)

b、代码调用exit函数,注意:我们的代码的任意位置调用exit(),都表示进程退出

c、_exit() -- 系统调用接口,代码的任意位置调用_exit(),也可以表示进程退出

exit()和_exit()的区别:exit()会在进程退出的时候,冲刷缓冲区,_exit()不会。

int main()
{
   printf("hello 111"); 
   sleep(2);
   exit(3);//会刷新缓存区,将内容打印出来
   _exit(3);//不会刷新缓存区,即不会让printf()函数里的字符串内容打印到屏幕上
}

缓冲区不在exit、_exit和操作系统里面,exit在底层调用的就是_exit,用户没有权利对操作系统内的各种字段做任何访问,包括终止或释放一个进程,所以exit底层一定要调用_exit(系统调用接口),所以,如果缓冲区在操作系统内部,那么exit肯定能刷新出来,_exit也能刷新出来,因为exit和_exit是调用关系。所以,缓冲区只能在_exit之上,在_exit之上的话,那么_exit是看不到数据,自然也就刷新不出来数据。

杀掉进程/进程退出:本质是释放进程的代码和数据,释放除了PCB之外的其它数据结构,就是对进程做管理的一种方式。

三、进程等待

进程等待必要性

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

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

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

进程等待的方法

wait方法

//wait的两个头文件
#include <sys/types.h>
#include <sys/wait.h>
// wait/waitpid:等待一个子进程的状态发生变化

// 函数的意义:等待父进程中,任意一个子进程的退出
pid_t wait(int* status) 
//返回值是等待成功时,子进程的pid;失败返回-1

//参数:输出型参数,获取子进程退出状态,不关心则可以设置为NULL
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

void ChildRun()
{
	int* p = NULL;
	int cnt = 5;
	while (1)
	{
		printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
		sleep(1);
		cnt--;
	}
}

int main()
{
	printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());

	pid_t id = fork();
	if (id == 0)
	{
		// child
		ChildRun();
		printf("child quit ...\n");
		exit(0);
	}
	sleep(7);//父进程休眠时,子进程呈现僵尸状态
	// fahter
	pid_t rid = wait(NULL);//等待子进程的pid,僵尸状态消失
	if (rid > 0)
	{
		printf("wait success, rid: %d\n", rid);
	}
}

waitpid方法

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
    当正常返回的时候waitpid返回收集到的是子进程pid;
    如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
    如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
   pid
        Pid=-1,等待任一个子进程。与wait等效。
        Pid>0.等待指定子进程的pid。
   status:
        WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
                   查看signal位(终止信号),当前的终止信号是不是为0,如果为0,是正常结束的;非0,条件不满足。
        WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
   options:
        options默认为0时,是阻塞等待
        WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。(非阻塞等待)
  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

第二个参数:输出型参数;需要你在你的代码当中定义一块内存空间,把空间的地址传进来,放入status指针变量中,然后在未来操作系统在等待时,它可以帮我们把子进程的退出信息,通过指针,带到你的用户层。子进程的退出信息。

int a;
scanf("%d ",&a);
// 在键盘上输入的数据,通过scanf()函数输出到你的a空间里。

获取子进程status

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特 位):

#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, cnt: %d\n", getpid(), getppid(), cnt);
		sleep(1);
		cnt--;
	}
}

int main()
{
	printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());

	pid_t id = fork();
	if (id == 0)
	{
		// child
		ChildRun();
		printf("child quit ...\n");
		exit(123);// 123是退出码,自己定的
	}
	sleep(7);
	// fahter
	//pid_t rid = wait(NULL);
	//pid_t rid = waitpid(-1, NULL, 0);
	//如果waitpid函数里的参数是这些的话,那么和上面的wait函数作用一样,都是等待任何一个子进程

	int status = 0;//初始化一下status
	pid_t rid = waitpid(id, &status, 0);//等待指定子进程pid的返回值
	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 quit signal: %d\n", status, (status >> 8) & 0xFF, status & 0x7F);
	// 0xFF:16进制,转换成二进制是8个1,按位与一下,可以把退出状态当中唯一的保留为0的直接去掉
	// 0x7F:01111111,按位与一下会把底16位中的前7位按位与出来
}

进程的阻塞等待

阻塞等待:
我们讲的scanf(),进程在等待键盘资源就绪;操作系统要管理硬件,也要先描述,再组织,所以每种设备在内核当中都有属于自己的结构体描述对象的,描述结构体对象里面包含等待队列的,所以进程等待就是将自己的PCB列入到设备的等待队列里就可以了。

如何理解阻塞等待子进程呢?
子进程本身就是软件,父进程本质是在等待某种软件条件就绪;当子进程没有退出,父进程就不被调度了,就是把父进程的状态设为S状态(非运行状态),把父进程的PCB列入到子进程的队列当中,此时,父进程就是在等待子进程。

什么叫进程阻塞呢?
把进程的状态由R状态设为非R状态,比如:S状态;把进程的PCB从运行队列移到其它的等待队列当中,不被调度就可以了。

int main()
{
	printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());

	pid_t id = fork();
	if (id == 0)
	{
		// child
		ChildRun();
		printf("child quit ...\n");
		exit(123);
	}

	sleep(7);
	//fahter
	//pid_t rid = wait(NULL); 等待任意一个子进程
	int status = 0;
	pid_t rid = waitpid(id, &status, 0);// 阻塞等待指定的子进程
	if(rid > 0)
	{
		// 子进程正常退出,没有异常(退出信号为0),为真
	    if(WIFEXITED(status))
	    {
			// WEXITSTATUS:获取子进程的退出码
	        printf("child quit success, child exit code : %d\n", WEXITSTATUS(status));
	    }
	    else
	    {
	        printf("child quit unnormal!\n");
	    }
	    printf("wait success, rid: %d\n", rid);
	}
	else
	{
	    printf("wait failed !\n");
	}
	sleep(3);
	printf("father quit, status: %d, child quit code : %d, child quit signal: %d\n", status, (status>>8)&0xFF, status & 0x7F);
}

进程的非阻塞等待(等待时,允许父进程做一些事情)

非阻塞等待:

pid_t waitpid(pid_t pid,int *status,int options)
  •  1、等待指定的进程;为-1时,等待任意进程;
  • 2、输出型参数;需要你在你的代码当中定义一块内存空间,把空间的地址传进来,放入status指针变量中,然后在未来操作系统在等待时,它可以帮我们把子进程的退出信息,通过指针,带到你的用户层。子进程的退出信息。
  • 3、默认为0时,就是阻塞等待; WNOHANG:宏,这个选项,以非阻塞的方式进行子进程的等待了。

返回值pid_t:

  1. pid_t > 0 :等待成功了,子进程退出了,并且父进程回收成功。
  2. pid_t < 0 :等待失败了。
  3. pid_t == 0 :检测是成功的,只不过子进程还没有退出,需要你下一次进行重复等待。

非阻塞等待的时候 + 循环 = 非阻塞轮询

int main()
{
	printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());

	pid_t id = fork();
	if (id == 0)
	{
		// child
		ChildRun();
		printf("child quit ...\n");
		exit(123);
	}
	LoadTask();
	// father
	while (1)
	{
		int status = 0;
		pid_t rid = waitpid(id, &status, WNOHANG); // 非阻塞等待:子进程没有退出,直接返回0,进行下一次循环等待
		if (rid == 0)
		{
			usleep(100000);
			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;
		}
	}
}

总结

好了,本篇博客到这里就结束了,如果有更好的观点,请及时留言,我会认真观看并学习。
不积硅步,无以至千里;不积小流,无以成江海。

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-04-23 22:30:03       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-23 22:30:03       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-23 22:30:03       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-23 22:30:03       20 阅读

热门阅读

  1. Python面试高频题

    2024-04-23 22:30:03       18 阅读
  2. Edge的使用心得与深度探索

    2024-04-23 22:30:03       28 阅读
  3. 利用vue3SeamlessScroll 简单实现列表的无限循环滚动

    2024-04-23 22:30:03       13 阅读
  4. 安卓一键换壁纸

    2024-04-23 22:30:03       11 阅读
  5. Android14 StatusBar顶部图标加载流程

    2024-04-23 22:30:03       11 阅读
  6. Spring Cloud Eureka面试题

    2024-04-23 22:30:03       13 阅读
  7. 【设计模式】观察者模式

    2024-04-23 22:30:03       13 阅读
  8. ecs150 project2 Unix Shell

    2024-04-23 22:30:03       13 阅读