Linux系统编程 day05 进程控制

news2024/11/30 15:35:10

Linux系统编程 day05 进程控制

  • 1. 进程相关概念
  • 2. 创建进程
  • 3. exec函数族
  • 4. 进程回收

1. 进程相关概念

程序就是编译好的二进制文件,在磁盘上,占用磁盘空间。程序是一个静态的概念。进程即使启动了的程序,进程会占用系统资源,如内存、CPU等,是一个动态的概念。

在一个时间段内,如果在同一个CPU上运行了多个程序,这就叫并发。在同一个时刻,如果CPU中运行了两个以及两个以上的程序,就叫并行,并行要求计算机要有多核CPU。

计算机的每一个进程中都有一个进程控制块(PCB)来维护进程的相关信息,Linux中的进程控制块是task_struct结构体。每一个进程都有一个唯一的ID,在C语言中用pid_t表示一个非负整数。每一个进程都有自己的状态,进程的状态有创建态、就绪态、运行态、挂起态、终止态等。在CPU发生进程切换的时候,PCB需要保存和恢复一些CPU寄存器的信息。

创建态就是进程刚创建的一个状态,随后会进入就绪态,一般常将就绪态和创建态结合着看。当就绪态的进程得到CPU的执行权分得时间片的时候,就会进入运行态,当时间片消耗完则会继续进入就绪态。在运行态的进程,如果遇到了sleep命令等就会进入挂起态,当sleep结束之后就会继续进入就绪态。就绪态的进程也会因为受到SIGSTOP信号而进入到挂起态。就绪态、运行态、挂起态三个状态的进程都有可能随时进入终止态,结束程序的运行。需要值得注意的是挂起态不能直接转到运行态,必须先转为就绪态。

在这里插入图片描述

2. 创建进程

在Linux中创建子进程我们使用fork函数。该函数的原型为:

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

       pid_t fork(void); // 创建一个子进程

其中该函数需要sys/types.hunistd.h这两个头文件。该函数不需要任何参数,返回值为子进程的pid,若失败了则会返回-1。经过该函数之后,我们可以得到两个pid,不是因为fork的返回值为两个,而是有两个进程在调用fork函数。因为我们创建了一个子进程,而本来就有一个进程。父进程调用fork函数会返回子进程的pid,而子进程调用fork函数返回的是0。所以在我们可以通过判断fork函数的返回值来确定究竟是子进程还是父进程。若pid小于0,则表明子进程创建失败,若pid等于0则说明该进程是子进程,若pid大于0则说明是父进程(返回的是子进程的pid)。

在创建的子进程的时候,操作系统会拷贝一份父进程的内存,内存分为内核区和用户区,其中用户区的数据内容是完全一样的,而内核区的内容不完全一样。比如pid就在内核区,因为每个进程都使用pid作为进程的唯一标识,所以不能一样。

在这里插入图片描述
在子进程创建了之后,父进程执行到了什么位置,子进程就会继续从该位置继续执行。两者的执行顺序并不一定是父进程就优先比子进程执行,也不是子进程一定优先比父进程执行,而是谁先抢到CPU的时间片谁就优先执行。

在这里插入图片描述

如何获得该进程的进程pid呢?操作系统为我们提供了两个函数。

       #include <sys/types.h>
       #include <unistd.h>
			
       pid_t getpid(void); // 获取当前运行进程的pid
       pid_t getppid(void); // 获取当前进程的父进程的pid

这两个函数第一个函数getpid是用于获取当前运行的进程的pid。而getppid是用于获取当前运行的进程的父进程的pid。

接下来我们来看一个使用fork函数的例子。

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

int main()
{
	printf("Before fork, pid: [%d]\n", getpid());
    
	//pid_t fork(void);
	// 创建子进程
	pid_t pid = fork();
	if(pid < 0)
	{
		perror("fork error");
		return -1;
	}
	else if(pid == 0)
	{
		// 子进程
		printf("child: pid: [%d], fpid: [%d]\n", getpid(), getppid());
	}
	else
	{
		// 父进程
		sleep(2);
		printf("father: pid: [%d], fpid: [%d]\n", getpid(), getppid());
	}

	printf("After fork, pid: [%d]\n", getpid());
	return 0;
}

前面我们说了在创建子进程的时候会拷贝一份父进程的内存,那么它们共享全局变量这些吗?实际上在多进程的程序中,它们做的是读时共享,写时复制。意思就是在不对内存的数据进行修改的时候它们是共享的,但是当你修改数据的时候操作系统会复制一份新的内存映射回去,再对这块复制的内存进行修改操作。所以父子进程不能共享全局变量。下面程序就验证了这个特性。

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

int g_var = 100;

int main()
{
	// 创建子进程
	pid_t pid = fork();
	if(pid < 0)
	{
		perror("fork error");
		return -1;
	}
	else if(pid > 0)
	{
		// 父进程
		printf("father: pid = [%d], fpid = [%d]\n", getpid(), getppid());
		g_var ++;
		printf("father: g_var = [%d], addr = [%p]\n", g_var, &g_var);
	}
	else
	{
		// 子进程
		sleep(1); // 避免父进程还没有执行子进程就已经结束
		printf("child: pid = [%d], fpid = [%d]\n", getpid(), getppid());
		printf("child: g_avr = [%d], addr = [%p]\n", g_var, &g_var);
	}
	return 0;
}

现在有一个问题,假如现在我要创建n个子进程,又应该怎么创建呢?假如是3个,那么我能否使用以下的语句进行创建呢?

for(int i = 0; i < 3; i ++)
{
	pid_t pid = fork();
}

从代码的表面上来看,的确是创建了3个子进程,实际上这里创建的远远不止3个子进程。分析以下原因是因为每一个子进程都会去执行fork函数。假如我们把父进程记为p0,当i=0时,p0会创建一个子进程p1。当i=1时,p0会创建子进程p2,p1会创建它的子进程p3。当i=2时,p0、p1、p2、p3都会分别创建一个子进程。综上,我们可以得出这里一共创建了7个子进程。也就是循环n次就会创建 2 n − 1 2^n-1 2n1个子进程。那么又如何该完成我们创建n个进程的任务呢?用循环是肯定的,但是我们在每次创建的时候都可以让子进程跳出循环,避免子进程创建新的子进程,让父进程一直循环创建。也即是在创建子进程之后,我们需要在子进程的运行代码中使用break语句。例如创建4个子进程,代码如下。

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

// 循环创建n个进程 
int main()
{
	int i = 0;
	for(i = 0; i < 4; i ++)
	{
		pid_t pid = fork();
		if(pid < 0)
		{
			// 创建失败
			perror("fork error");
			return -1;
		}
		else if(pid > 0)
		{
			// 父进程
			printf("father--pid:[%d]--fpid:[%d]\n", getpid(), getppid());
		}
		else 
		{
			// 子进程
			printf("child--pid:[%d]--fpid:[%d]\n", getpid(), getppid());
			break;
		}
	}

	if(i != 4)
	{
		printf("[%d]--pid:[%d]--fpid:[%d]---child\n", i, getpid(), getppid());
	}
	else
	{
		printf("[%d]--pid:[%d]--fpid:[%d]---father\n", i, getpid(), getppid());
	}

	sleep(4);
	return 0;
}

在Linux的shell中,我们常用ps去查看当前还在运行的进程,以及用kill去杀死某个进程。在ps中,常用的参数由以下四个。

参数作用
-a当前系统的所有用户进程
-e当前系统的所有进程,作用与-a一样
-f按照完整格式列表显示
-u查看进程所有者以及其它一些信息
-x显示没有控制终端的进程,也就是不能与用户进行交互的进程
-j列出与作业控制相关的信息

kill中,我们会使用-9或者-15去杀死某个进程,这里的数字是一些信号。在Linux中的信号有以下:

在这里插入图片描述

3. exec函数族

有时候我们需要一个进程里面去执行其它的命令或者是用户的自定义程序,这个时候就需要我们使用exec函数族中的函数。使用的一般方法都是先在父进程中创建子进程,然后在子进程中调用exec函数。exec函数族的常用函数原型如下:

       #include <unistd.h>

       int execl(const char *pathname, const char *arg, .../* (char  *) NULL */);
       int execlp(const char *file, const char *arg, .../* (char  *) NULL */);

这些函数的作用是在子进程中在执行自定义应用程序或者命令。

函数名参数返回值作用
execlpathname:文件路径名
arg:占位参数
…:程序的外部参数
成功不返回,失败返回-1并设置errno在子进程中执行路径pathname指定的程序
execlpfile:文件名
arg:占位参数
…:程序的外部参数
成功不返回,失败返回-1并设置errno在子进程中执行file文件

上面两个exec函数中的第二个参数arg是占位参数,一般写成和第一个参数一样的,这个参数的作用在于使用ps查询进程的时候可以看到进程名为arg的值。后面的...为执行的外部参数,比如我们使用ls命令的时候需要按照时间顺序逆序排序则需要写成-ltr。在...写完之后,必须写上一个NULL表示参数结束。

一般我们使用execl函数来执行自定义应用程序,而使用execlp来执行内部的命令。使用execlp的时候,第一个file参数会根据系统的PATH变量的值来进行搜索。

使用execl的示例如下:

#include <stdio.h>
#include <unistd.h>

int main()
{
	pid_t pid = fork();
	if(pid < 0)
	{
		perror("fork error");
		return -1;
	}
	else if(pid == 0)
	{
		execl("helloworld", "helloworld", NULL);
	}
	else if(pid > 0)
	{
		printf("father: pid = [%d], fpid = [%d]\n", getpid(), getppid());
		sleep(20);
	}
	return 0;
}

使用execlp的示例如下:

#include <stdio.h>
#include <unistd.h>

int main()
{
	pid_t pid = fork();
	if(pid < 0)
	{
		perror("fork error");
		return -1;
	}
	else if(pid == 0)
	{
		sleep(4);
		printf("child --- pid = [%d] --- fpid = [%d]\n", getpid(), getppid());
		execlp("ls", "ls", "-ltr", NULL);
	}
	else
	{
		sleep(10);
		printf("father --- pid = [%d] --- fpid = [%d]\n", getpid(), getppid());
	}
	return 0;
}

4. 进程回收

当一个进程退出之后,进程能够回收自己用户区的资源,但是不能回收内核空间的PCB资源,这个必须要它的父进程调用wait或者是waitpid函数完成对子进程的回收,避免造成系统资源的浪费。这两个函数在后面会进行介绍。

在一个程序中,如果父进程已经死了,而子进程还活着,那么这个子进程就成为了孤儿进程。为了保证每一个进程都有一个父进程,孤儿进程会被init进程领养,init进程就会成为子进程的养父进程,当孤儿进程退出之后,由init程序完成对孤儿进程的回收。需要注意的是在某些使用Systemd来管理系统的Ubuntu上就可能是由systemd进程来收养,而不是init进程。如下面就是一个孤儿进程的案例。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>


int main()
{
	pid_t pid = fork();
	if(pid < 0)
	{
		perror("fork error");
		return -1;
	}
	else if(pid > 0)
	{
		sleep(3);
		printf("father: pid = [%d], fpid = [%d]\n", getpid(), getppid());
	}
	else
	{
		printf("child: pid = [%d], fpid = [%d]\n", getpid(), getppid());
		sleep(10);
		printf("child: pid = [%d], fpid = [%d]\n", getpid(), getppid());
	}

	return 0;
}

如果在一个程序中,子进程已经死了,而父进程还活着,但是父进程没有用wait或者waitpid对子进程进行回收,则这个子进程就会称为僵尸进程。

在这里插入图片描述僵尸进程在用ps进行查询的时候会有<defunct>标识。由于僵尸进程是一个已经死亡了的进程,所以我们不能使用kill进行杀死,那么怎么解决僵尸进程的问题呢?

第一个解决方法是将它的父进程给杀死,因为父进程死亡之后僵尸进程会被init进程所领养,然后被init进程回收其资源。第二个方法就是在父进程中调用wait或者waitpid函数进行回收子进程的资源。这两个函数的原型如下:

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

       pid_t wait(int *wstatus);
       pid_t waitpid(pid_t pid, int *wstatus, int options);

函数名参数返回值作用
waitwstatus:子进程的退出状态成功返回清理掉的子进程pid,失败返回-1(没有子进程)阻塞并等待子进程的退出,回收子进程残留的资源,获取子进程结束的状态退出原因
waipidpid:需要回收的进程pid
wstatus:子进程的退出状态
option:阻塞或者非阻塞,设置WNOHANG为非阻塞,设置为0表示阻塞
返回值大于0表示回收掉的子进程的pid,返回值为-1表示没有子进程,返回值为0且option为WNOHANG的时候表示子进程正在运行阻塞并等待子进程的退出,回收子进程残留的资源,获取子进程结束的状态退出原因

在上面的waitpid函数中,若pid=-1表示等待任一子进程;若pid>0表示等待其进程ID与pid相等的子进程;若pid=0表示等待进程组ID与当前进程相同的任何子进程;若pid<-1表示等待其组ID等于pid的绝对值的任一子进程(适用于子进程在其它组的情况)。

若我们不关心子进程的返回状态以及返回值,则可以将wstatus传为NULLwstatus的操作内容比较多,下面介绍两个常用的。

操作作用
WIFEXITED(wstatus)为非0表示程序正常结束
WEXITSTATUS(wstatus)获取进程的退出状态也就是返回值
WIFSIGNALED(wstatus)为非0表示程序异常终止
WTERMSIG(wstatus)获取进程终止的信号编号

下面给出一个这两个函数的使用案例。使用wait回收子程序资源的例子:

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

int main()
{
	pid_t pid = fork();
	if(pid < 0)
	{
		perror("fork error");
		return -1;
	}
	else if(pid > 0)
	{
		// 父进程
		printf("father: pid = [%d], fpid = [%d]\n", getpid(), getppid());
		int wstatus;
		pid_t wpid = wait(&wstatus);
		if(wpid < 0)
		{
			printf("There are no child processes to reclaim\n");
		}
		else 
		{
			if(WIFEXITED(wstatus))
			{
				// 正常退出
				printf("The process terminated normally, return = [%d]\n", WEXITSTATUS(wstatus));
				printf("Reclaim to child process wpid = [%d]\n", wpid);
			}
			else if(WTERMSIG(wstatus))
			{
				// 被信号杀死
				printf("The process is killed by signal, signal is [%d]\n", WTERMSIG(wstatus));
				printf("Reclaim to child process wpid = [%d]\n", wpid);
			}
		}
	}
	else
	{
		// 子进程
		printf("child: pid = [%d], fpid = [%d]\n", getpid(), getppid());
		sleep(15);
		return 100;
	}
	return 0;
}

使用waitpid的示例代码如下:

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

int main()
{
	srand(time(NULL));
	printf("father: pid = [%d], fpid = [%d]\n", getpid(), getppid());
	pid_t f_pid = getpid(); // 父亲的pid
	for(int i = 0; i < 4; i ++)
	{
		sleep(rand() % 2);
		pid_t pid = fork();
		if(pid < 0)
		{
			perror("fork error");
			return -1;
		}
		else if(pid == 0)
		{
			printf("child: pid = [%d], fpid = [%d]\n", getpid(), getppid());
			break;
		}
	}

	if(getpid() == f_pid)
	{
		// 父进程
		pid_t wpid = 0;
		int wstatus = 0;
		// 等待任意一个子进程,非阻塞
		while((wpid = waitpid(-1, &wstatus, WNOHANG)) != -1)
		{
			// 有进程死亡
			if(wpid > 0)
			{
				if(WIFEXITED(wstatus))
				{
					// 正常死亡
					printf("The process [%d] terminated normally, return = [%d]\n", wpid, WEXITSTATUS(wstatus));
				}
				else if(WIFSIGNALED(wstatus))
				{
					// 信号杀死
					printf("The process [%d] is killed by signal [%d]\n", wpid, WTERMSIG(wstatus));
				}
			}
		}
	}
	else
	{
		// 子进程
		int s_time = rand() % 10 + 10;
		int r_number = rand() % 10;
		printf("The process [%d], the father is %d, return [%d], sleep time [%d]s\n", getpid(), getppid(), r_number, s_time);
		sleep(s_time);
		return r_number;
	}
	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1255818.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

win11任务栏居中/靠左设置路径

win11任务栏居中/靠左设置路径 设置-个性化-任务栏-任务栏对齐方式

Proteus仿真--基于字符液晶显示的频率计

本文介绍基于数码管的频率计&#xff08;完整仿真源文件及代码见文末链接&#xff09; 仿真图如下 本设计中80C51单片机作为主控&#xff0c;用字符液晶作为显示模块&#xff0c;按下按键K1后可进行频率测量并显示 仿真运行视频 Proteus仿真--基于字符液晶显示的频率计 附完…

只狼 资源分享

版本介绍 v1.06版|容量15GB|官方简体中文|支持键盘.鼠标.手柄|赠官方原声4首BGM|赠多项修改器|赠一周目全义手忍具强化通关存档|2020年01月15号更新 只狼中文设置&#xff1a; https://jingyan.baidu.com/article/cb5d6105bc8556005d2fe048.html 只狼键盘对应按键&#xff1…

6.1 Windows驱动开发:内核枚举SSDT表基址

SSDT表&#xff08;System Service Descriptor Table&#xff09;是Windows操作系统内核中的关键组成部分&#xff0c;负责存储系统服务调用的相关信息。具体而言&#xff0c;SSDT表包含了系统调用的函数地址以及其他与系统服务相关的信息。每个系统调用对应SSDT表中的一个表项…

RocketMQ相关概念与使用入门详解

文章目录 RocketMQ 相关概念消息模型MQ 的简单消息模型RocketMQ 的复杂消息模型 RocketMQ 中消息相关概念消息&#xff08;Message&#xff09;主题&#xff08;Topic&#xff09;Tags队列消息标识 RocketMQ 中的物理对象NameServerBrokerProducerConsumer NameServer 与 Broke…

ARM推出Cortex-M85的小弟Cortex-M52, 集低功耗,低成本和单片机AI于一身

Cortex-M52特色&#xff1a; 1、基于ARMv8.1-M架构的内核已经有M55和M85. 新出的M52是采用Arm Helium 技术的最小处理器&#xff0c;可提供出色的低功耗&#xff0c;为物联网提供低成本和高性能AI技术。 2、Cortex-M52 专为需要提高数字信号处理和机器学习性能的 AIoT 应用而…

王者荣耀Java

代码 package com.sxt;import javax.swing.*; import java.awt.*;public class Background extends GameObject {public Background(GameFrame gameFrame) {super(gameFrame);// TODO Auto-generated constructor stub}Image bg Toolkit.getDefaultToolkit().getImage("…

《C++PrimePlus》第10章 对象和类

10.1 过程性编程和面向对象编程 10.2 抽象和类 10.3 类的构造函数和析构函数 类的定义和使用&#xff08;买卖股票&#xff09; 头文件stock10.h #ifndef __STOCK00__H__ #define __STOCK00__H__#include <string>class Stock { // pravate的内容只能通过public访问 p…

王者荣耀java版

主要功能 键盘W,A,S,D键&#xff1a;控制玩家上下左右移动。按钮一&#xff1a;控制英雄发射一个矩形攻击红方小兵。按钮二&#xff1a;控制英雄发射魅惑技能&#xff0c;伤害小兵并让小兵停止移动。技能三&#xff1a;攻击多个敌人并让小兵停止移动。普攻&#xff1a;对小兵造…

坚鹏:广州银行清华大学消费金融发展趋势与创新培训圆满结束

广州银行自1996年9月成立以来&#xff0c;依托中国经济腾飞的大好形势&#xff0c;成为国内具有一定知名度与地方特色的商业银行。截至2022年12月末&#xff0c;已开业机构174家&#xff0c;包括总行1家&#xff0c;分行级机构15家(含信用卡中心)、支行152家、信用卡分中心6家&…

STM32 外部中断配置与中断函数设计

单片机学习 目录 文章目录 一、外部中断配置步骤 1.1配置RCC 1.2配置GPIO 1.3配置AFIO 1.4配置EXTI 1.5配置NVIC 二、中断函数设计 总结 一、外部中断配置步骤 第一步&#xff1a;配置RCC&#xff0c;把涉及外设的时钟打开。第二步&#xff1a;配置GPIO&#xff0c;选择…

企业如何创建和运营在线知识库?

随着企业业务的不断扩展和复杂化&#xff0c;建立一个在线知识库已经成为企业提高效率、减少重复劳动和提升服务质量的重要手段。接下来就详细介绍一下企业如何创建和运营在线知识库。 | 一、明确知识库的需求和目标 在开始建立在线知识库之前&#xff0c;企业需要明确知识库的…

ClickHouse中的物化视图

技术主题 技术原理 物化视图&#xff08;Materialized View&#xff09;是一种预先计算并缓存结果的视图&#xff0c;存储在磁盘上自动更新&#xff0c;空间换时间的思路。物化视图是一种优化技术&#xff0c;本质上就是为了加速查询操作&#xff0c;降低系统负载&#xff0c…

操作系统——解决了我的一些困惑

目录 1、电脑开机做了什么事情 2、真正实现并行的计算机 3、计算机中的淘汰算法 & 分配算法 & 调度算法 & 空间管理 4、什么是虚拟内存&#xff1f;为什么需要虚拟内存&#xff1f;最多可分配多少&#xff1f; 5、TLB&#xff08;快表&#xff09;、分页存储&…

React入门使用 (官方文档向 Part1)

文章目录 React组件:万物皆组件 JSX: 将标签引入 JavaScriptJSX 规则1. 只能返回一个根元素2. 标签必须闭合3. 使用驼峰式命名法给 ~~所有~~ 大部分属性命名&#xff01;高级提示&#xff1a;使用 JSX 转化器 在 JSX 中通过大括号使用 JavaScript使用引号传递字符串使用大括号&…

佳易王幼儿园缴费系统软件编程应用实例

佳易王幼儿园缴费系统软件编程实例 佳易王幼儿园缴费系统功能&#xff1a; 1、系统设置 2、班级设置 3、其他费用名称 4、学生信息管理 5、学生缴费 6、统计报表 7、备份全部数据 软件试用版下载可以点击下方官网卡片

前端向后端传JSON数据,使用MyBatis查询

form中向后端传的是空字符串&#xff0c;并不是null 而在MyBatis的判断中应判断是否为空字符串&#xff0c;而并非null

2016年五一杯数学建模B题能源总量控制下的城市工业企业协调发展问题解题全过程文档及程序

2016年五一杯数学建模 B题 能源总量控制下的城市工业企业协调发展问题 原题再现 能源是国民经济的重要物质基础,是工业企业发展的动力&#xff0c;但是过度的能源消耗&#xff0c;会破坏资源和环境&#xff0c;不利于经济的可持续发展。目前我国正处于经济转型的关键时期&…

String,StringBuffer以及StringBuilder之间的区别

文章目录 区别一&#xff1a;字符串内容是否可变区别二&#xff1a;线程安全区别三&#xff1a;性能区别四&#xff1a;使用场景 “String,StringBuffer以及StringBuilder之间的区别的区别” 这个问题是面试官比较常问的一个Java基础题。在日常的开发中&#xff0c;也常常会用到…

Vue轻松入门,附带学习笔记和相关案例

目录 一Vue基础 什么是Vue&#xff1f; 补充&#xff1a;mvvm框架 mvvm的组成 详解 Vue的使用方法 1.直接下载并引入 2.通过 CDN 使用 Vue 3.通过npm安装 4.使用Vue CLI创建项目 二插值表达式 什么是插值表达式&#xff1f; 插值表达式的缺点 解决方法 相关代…