【嵌入式Linux】<总览> 多进程(更新中)

news2024/11/16 19:44:58

文章目录

前言

一、进程的概念与结构

1. 相关概念

2. 内核区中的进程结构

 3. 进程的状态

4. 获取进程ID函数

二、进程创建

1. fork和vfork函数

2. 额外注意点

3. 构建进程链

4.构建进程扇

 三、进程终止

1. C程序的启动过程

2. 进程终止方式

四、特殊的进程

1. 僵尸进程

2. 守护进程

3. 孤儿进程

五、相关函数

1. wait函数

2. waitpid函数

3. execl函数

4. execlp函数


前言

在Linux中程序的运行涉及进程的相关知识,熟悉并掌握其相关知识在嵌入式Linux应用开发中至关重要。本篇记录进程的具体知识,若涉及版权问题,请联系本人删除!


一、进程的概念与结构

1. 相关概念

  • 程序:存放在硬盘的可执行文件。
  • 进程:是程序运行的实例,每个进程都有一个虚拟地址空间。进程之间相互独立,同时也存在相关机制来进行进程的通信。每个Linux进程都有唯一的进程ID(PID),其都是正整数。
  • 并发:虚假的同时运行多个进程,是单CPU切换速度极快的结果。
  • 并行:真实的同时运行多个进程,有多个CPU。
  • 命令:①如下图,通过命令"ps -aux"可以查看进程信息。②用kill -9可以强制退出进程。

2. 内核区中的进程结构

每启动一个进程,在虚拟地址空间的内核区中就会对应一个task_struct结构体(进程控制块PCB),如下图所示。其中包含了进程的ID、状态、优先级、调度策略、文件结构体指针(指向文件描述符表)等等。

 3. 进程的状态

有五种常见状态:创建态、就绪态、运行态、阻塞态(挂起态)和退出态(终止态)。

  • 创建态:进程在创建时就是该状态,时间很短。
  • 就绪态:创建后就处于该状态,等待抢夺CPU时间片。
  • 运行态获得CPU资源使得该进程运行,当时间片用完后重新回到就绪态。
  • 阻塞态:进程强制放弃CPU,无法抢夺CPU时间片(例如sleep在休眠期间)。同时,阻塞态又分为不可中断和可中断类型。(执行中按下Ctrl+C能中断的是可中断类型)
  • 退出态:进程的终止,占用的系统资源被释放。(任何状态都可以直接转换为退出态)

僵尸状态:进程已经终止了,用户区资源已经被释放了,但是内核区中的task_struct仍有信息,ps的命令中STAT值为Z。

4. 获取进程ID函数

#include <unistd.h>
#include <sys/types.h>
当前进程ID: pid_t getpid(void);
当前进程的父进程ID: pid_t getppid(void);
当前进程的实际用户ID: uid_t getuid(void);
当前进程的有效用户ID: uid_t geteuid(void);
当前进程的用户组ID: gid_t getgid(void);
当前进程的进程组ID: pid_t getpgrp(void);
进程ID为pid的进程组ID: pid_t getpgid(pid_t pid);

【注】实际用户是当前环境下的用户,有效用户是真正开启进程的用户


二、进程创建

1. fork和vfork函数

【1】头文件:#include <sys/types.h>、#include <unistd.h>

【2】函数原型:①pid_t fork(void); ②pid_t vfork(void);

【3】功能:

  • fork创建子进程,且子进程复制父进程的内存空间。子、父进程谁先运行看进程调度。
  • vfork创建子进程,子进程先运行不复制父进程空间。

2. 额外注意点

  • fork和vfork被调用一次,会返回两次:子进程中的返回值为0,在父进程中的返回值则是子进程的PID。可以根据返回值不同来区分是父进程还是子进程。
  • 失败返回值:创建子进程失败会返回-1。
  • 执行位置:父进程是从main函数代码体首部开始执行,子进程是从fork函数之后开始执行。
  • 虚拟地址空间的用户空间:子进程中代码段与环境变量的物理空间和父进程是同一个。而其他的物理空间不是同一个(而是将父进程的复制一份给子进程),即使它们的虚拟地址是一样的。
  • 虚拟地址空间的内核空间:①子进程只复制父进程的文件描述符表,不复制但共享文件表项和inode。②父进程创建一个子进程后,文件表项中的引用计数器加1,当父进程close后计数器减1,子进程还是可以使用文件表项,只有当计数器为0时才会释放文件表项。

实验程序1:创建子进程,打印子、父进程中的pid信息。

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

int main(int argc, char **argv)
{
	//fork创建子进程,复制父进程空间
	pid_t pid = fork();

	//子、父进程中打印pid
	if (pid < 0) {
		perror("创建子进程失败");
	} else if (pid == 0) {//子进程
		printf("I am child process. PID: %d, PPID: %d, 返回的PID: %d\n", getpid(), getppid(), pid);
	} else {//父进程
		printf("I am parent process. PID: %d, PPID: %d, 返回的PID: %d\n", getpid(), getppid(), pid);
	}
	return 0;
}

实验程序2:父进程将文件指针定位到文件尾部,子进程写入内容。原有目录下有文件1.txt,原有内容为123

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char **argv)
{
	//命令行参数判定
	if (argc != 2) {
		printf("Command: %s <filename>\n", argv[0]);
		return -1;
	}

	//文件操作
	int fd = open(argv[1], O_WRONLY);
	if (fd < 0) {
		perror("文件打开错误");
		return -1;
	}

	//父进程改变文件指针到文件尾部
	//子进程等待父进程定位好后写入内容
	pid_t pid = fork();
	if (pid < 0) {
		perror("创建子进程错误");
		close(fd);
		return -1;
	} else if (pid > 0) {//父进程
		if (lseek(fd, 0, SEEK_END) < 0) {
			perror("文件指针定位错误");
			close(fd);
			return -1;
		}
	} else {//子进程
		sleep(2);//确保父进程先运行
		const char * content = "Hello, Can!\n";
		int contentSize = strlen(content);
		if (write(fd, content, contentSize) < contentSize) {
			printf("写入错误\n");
			close(fd);
			return -1;
		}
	}
	printf("--------pid: %d完成工作---------\n", getpid());

	//关闭文件:父子进程都会关闭,使得引用计数减为0
	close(fd);
	return 0;
}

3. 构建进程链

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

int main(int argc, char **argv)
{
	//创建3个子进程,形成进程链
	for (int i = 0; i < 3; ++i) {
		pid_t pid = fork();
		if (pid < 0) {
			perror("创建失败");
			return -1;
		}
		if (pid > 0) { //若为父进程则退出
			break;
		}
	}
	printf("PID: %d, PPID: %d\n", getpid(), getppid());
	sleep(1);
	return 0;
}

4.构建进程扇

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

int main(int argc, char **argv)
{
	//创建3个子进程,形成进程扇
	for (int i = 0; i < 3; ++i) {
		pid_t pid = fork();
		if (pid < 0) {
			perror("创建失败");
			return -1;
		}
		if (pid == 0) {//若为子进程则退出
			break;
		}
	}
	printf("PID: %d, PPID: %d\n", getpid(), getppid());
	sleep(1);
	return 0;
}


 三、进程终止

1. C程序的启动过程

在main函数执行前,Linux内核会启动一个特殊例程,将命令行中的参数传给argc和argv。若主函数中有三个形参,那么该例程还会将环境信息构建成环境表传给第三个形参。最后,该例程还会登记进程的终止函数(进程终止前会调用)。

终止函数说明:

  • 每个进程都默认登记了一个标准的终止函数。
  • 终止函数在进程终止时释放一些资源。
  • 登记的多个终止函数的执行顺序按照的方式执行。
  • 用户自定义终止函数(无参无返回值),需要调用atexit函数向内核登记。

atexit函数:

【1】头文件:#include <stdlib.h>

【2】功能:向内核登记一个终止函数,该函数会在正常进程终止时被调用。

【3】函数原型:int atexit(void (*function)(void));

【4】返回值:成功返回0,否则返回非零值。

2. 进程终止方式

  • 正常终止:
    • ①main函数中return返回 会刷新标准IO缓存,会执行自定义的终止函数
    • ②调用库函数exit(0) 会刷新标准IO缓存,会执行自定义的终止函数
    • ③调用系统调用函数_exit(0)或_Exit(0) 不会刷新标准IO缓存,不会执行自定义的终止函数
    • ④最后一个线程从其启动例程返回
    • ⑤最后一个线程调用库函数pthread_exit
  • 异常终止:
    • ①调用库函数abort
    • ②接收到信号并终止(例如段错误会产生一个信号,然后终止进程)
    • ③最后一个线程对取消请求做处理响应

实验程序:运行下列代码,若参数指定为exit或return,文件中有写入的字符串,并且会执行自定义的终止函数;若参数指定为_exit,文件中没有任何内容,并且没有执行终止函数。

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

//自定义终止函数
void fun1() {
	printf("Terminate: fun1\n");
}

void fun2() {
	printf("Terminate: fun2\n");
}

void fun3() {
	printf("Terminate: fun3\n");
}

//主函数
int main(int argc, char **argv)
{
	//命令行参数判定
	if (argc != 3) {
		printf("commnd: %s <filename> <exit | return | _exit>\n", argv[0]);
		return -1;
	}

	//登记自定义终止函数
	atexit(fun1);
	atexit(fun2);
	atexit(fun3);

	//文件操作,忽视健壮性判定
	FILE *fd = fopen(argv[1], "w");//文件不存在则创建,调用失败返回NULL
	fprintf(fd, "Hello, world!\n");//向文件缓冲区写入字符串,若没有刷新或fclose则不会写入硬盘

	//根据参数选择退出方式
	if (!strcmp(argv[2], "exit")) {
		exit(0);
	} else if (!strcmp(argv[2], "return")) {
		return 0;
	} else {
		_exit(0);
	}
}


四、特殊的进程

1. 僵尸进程

  • 概念:子进程的虚拟地址空间中的用户区资源已经释放,但内核区中的task_struct没有被释放,那么该进程就是僵尸进程。
  • 释放僵尸进程的方式:
    • ①结束或kill僵尸进程的父进程,那么僵尸进程就会成为孤儿进程,然后会被init进程(1号进程)领养,最终会被回收。
    • ②让僵尸进程的父进程来回收。父进程每隔一段时间就查询子进程是否结束并回收,调用wait函数或waitpid函数,通过内核来释放僵尸进程。
    • ③采用信号SIGCHLD通知处理,在信号处理函数中调用wait函数。

程序示例:运行如下程序,就会生成僵尸进程。

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

int main(int argc, char **argv)
{
	//创建子进程
	pid_t pid = fork();
	if (pid < 0) {
		perror("创建子进程失败");
		return -1;
	}
	//子进程退出,成为僵尸进程
	if (pid == 0) {
		printf("PID: %d, PPID: %d\n", getpid(), getppid());
		return -1;
	}
	//父进程循环,便于观察
	while(1) {
		sleep(1);
	}
	return 0;
}

2. 守护进程

  • 概念:是一种生存期很长的进程。从操作系统启动开始,在操作系统关闭时终止。
  • 所有守护进程都以root(用户ID为0)的优先权运行。
  • 守护进程没有控制终端,一直在后台运行。
  • 守护进程的父进程都是init进程。

3. 孤儿进程

  • 概念:父进程结束了,但是子进程还在运行,那么此时子进程就是孤儿进程。孤儿进程由init进程(1号进程)来回收。
  • 领养机制引入:进程的用户区资源可以自己释放,但是内核区资源需要由父进程释放。而孤儿进程的父进程已经结束。因此,为了释放孤儿进程的内核区资源,让1号进程来领养它,进而释放其内核区的task_struct结构体。

程序示例:通过fork创建子进程,同时让父进程退出,那么子进程就是孤儿进程。

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

int main(int argc, char **argv)
{
	//创建子进程
	pid_t pid = fork();
	if (pid < 0) {
		perror("创建子进程失败");
		return -1;
	}
	//父进程退出
	if (pid > 0) {
		printf("PID: %d, PPID: %d\n", getpid(), getppid());
		return -1;
	}
	//子进程成为孤儿进程
	if (pid == 0) {
		sleep(2);
		printf("PID: %d, PPID: %d\n", getpid(), getppid());
		return -1;
	}
	return 0;
}


五、相关函数

1. wait函数

【1】头文件:#include <sys/types.h>、#include <sys/wait.h>

【2】函数原型:pid_t wait(int *wstatus);

【3】参数说明:wstatus是传出的参数,存放子进程退出时的信息。例如:wait(7status);

取出整形变量status中的数据需要使用一些宏函数:

  • WIFEXITED(status)用于判定是否是正常结束,是的话返回真;WEXITSTATUS(status)取出对应的进程退出码。
  • WIFSIGNALED(status)用于判定是否是异常结束,是的话返回真;WTERMSIG(status)取出对应的进程退出码。
  • WIFSTOPPED(status)用于判定是否是暂停子进程的返回,是的话返回真;WSTOPSIG(status)取出对应的进程退出码。

【4】功能:父进程等待子进程退出并回收,避免僵尸进程和孤儿进程产生。

【5】返回值:成功则返回子进程的PID,失败返回-1

【6】注意:wait函数等待所有的子进程退出。

示例程序:演示子进程异常退出,父进程对退出码进行处理。

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

int main(int argc, char **argv)
{
	//创建子进程
	pid_t pid = fork();
	if (pid < 0) {
		perror("创建子进程失败");
		return -1;
	}
	//子进程:打印信息,异常退出
	if (pid == 0) {
		printf("PID: %d, PPID: %d\n", getpid(), getppid());
		int i = 3, j = 0, k = i/j;//由于除0异常退出
	}
	//父进程:阻塞等待子进程退出,将退出码保存
	int status;
	pid_t ret = wait(&status);
	if (ret < 0) {
		printf("回收失败\n");
	} else {
		printf("回收成功,子进程PID:%d\n", ret);
	}
	//父进程:处理退出码
	if(WIFEXITED(status)) {
		printf("正常退出:%d\n", WEXITSTATUS(status));
	} else if (WIFSIGNALED(status)) {
		printf("异常退出:%d\n", WTERMSIG(status));
	} else if (WIFSTOPPED(status)) {
		printf("暂停退出:%d\n", WSTOPSIG(status));
	} else {
		printf("未知退出\n");
	}
	return 0;
}

2. waitpid函数

3. execl函数

4. execlp函数

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

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

相关文章

AppInventor2添加超过10个屏幕会怎样?

之前发过一篇AppInventor2官方翻译文档&#xff0c;建议一个项目不要超过10个屏幕&#xff0c;详见&#xff1a; App Inventor 2 构建多屏幕App的最佳实践 App Inventor 可以轻松地向应用程序添加更多屏幕&#xff0c;但最好也不要添加太多屏幕&#xff0c;因为多个屏幕的应用…

U盘数据恢复全攻略:从原理到实践

一、引言&#xff1a;为何U盘数据恢复至关重要 在信息化时代&#xff0c;U盘作为便携存储设备&#xff0c;广泛应用于各个领域。然而&#xff0c;U盘数据的丢失往往给个人和企业带来极大的困扰。数据丢失的原因多种多样&#xff0c;可能是误删除、格式化、文件系统损坏&#x…

探索约束LLM输出JSON的应用

0、 引言 JSON&#xff08;JavaScript Object Notation&#xff09;因其简洁、易读和易于解析的特性&#xff0c;已成为全球使用最广泛的数据交换格式之一。它能够满足各种数据交换需求&#xff0c;特别是在构建人工智能驱动的应用程序时&#xff0c;工程师们经常需要将大型语…

Jenkins教程-8-上下游关联自动化测试任务构建

上一小节小节我们学习了一下Jenkins自动化测试任务发送测试结果邮件的方法&#xff0c;本小节我们讲解一下Jenkins上下游关联自动化测试任务的构建。 下面我们以一个真实的自动化测试场景来讲解Jenkins如何管理上下游关联任务的触发和构建&#xff0c;比如我们有两个jenkin任务…

基础入门篇 | YOLOv10 项目【训练】【验证】【推理】最简单教程 | YOLOv10必看 | 最新更新,直接打印 FPS,mAP50,75,95

文章目录 训练 --train.py推理 --detect.py验证 --val.py不训练,只查看模型结构/参数量/计算量 --test.pyYOLOv10 是基于 YOLOv8 项目的改进版本,目前已经被 YOLOv8 项目合并,所以两个算法使用方法完全一致~ 今天我给大家展示一种非常方便的使用过程,包含【训练】【验证】…

情绪管理篇:让七情自然流露,不过分压抑也不掺杂极端的想法即可来去自如

情绪管理篇&#xff1a; 人有七情&#xff0c;本属常理&#xff0c;该哭的时候哭、该笑的时候笑、该怒的时候怒、该忧的时候忧 学习圣贤之学&#xff0c;并非让我们像木头人一样&#xff0c;枯木死灰&#xff0c;而要让自己不要被七情所缠缚、被七情所乱心&#xff0c;我们的喜…

QT拖放事件之三:自定义拖放操作-利用QDrag来拖动完成数据的传输

1、运行效果 1)Qt::MoveAction 2)Qt::CopyAction 2、源码 #include "Widget.h" #include "ui_Widget.h" #include "common.h"

JDBC的概念 ,核心API的介绍 , 注册驱动介绍

第一章 JDBC 1、JDBC的概念 目标 能够掌握JDBC的概念能够理解JDBC的作用 讲解 客户端操作MySQL数据库的方式 使用第三方客户端来访问MySQL&#xff1a;SQLyog、Navicat 使用MySQL自带的命令行方式 通过Java来访问MySQL数据库&#xff0c;今天要学习的内容 如何通过Java代…

考研数学|《李林880》正确率多少算合格?

李林880题是针对考研数学三的练习题集&#xff0c;覆盖了考研数学三的主要知识点和题型。如果能够熟练掌握这些题目&#xff0c;意味着对考研数学三的知识点有了较为深入的理解和应用能力。 首先&#xff0c;考研数学三的总分是150分&#xff0c;题型包括单选题、填空题和解答…

Day5 —— 电商日志数据分析项目

项目二 _____&#xff08;电商日志数据分析项目&#xff09; 引言需求分析详细思路统计页面浏览量Map阶段Reduce阶段 日志的ETL操作Map阶段Reduce阶段 统计各个省份的浏览量Map阶段Reduce阶段 具体步骤统计页面浏览量日志的ETL操作统计各个省份的浏览量工具类&#xff08;utils…

鸿蒙HarmonyOS服务卡片实战

引言 在现代开发中&#xff0c;服务卡片是不可或缺的一部分&#xff0c;比如音乐&#xff0c;天气类等应用&#xff0c;官网的介绍中写道&#xff1a;卡片让您便捷地预览服务信息&#xff0c;例如查看天气或日历日程等内容。您可将卡片添加到屏幕上&#xff0c;让这类信息触手…

拼多多面试总结

文章目录 一面自我介绍提问算法反问结果 二面提问算法反问结果 主管面主管面试准备算法题其他个人提问准备 提问数据库普通索引和覆盖索引的区别索引是什么&#xff1f;索引怎么加快数据库查询的&#xff1f;索引具体怎么实现的&#xff1f;以B树为例&#xff0c;节点放了什么&…

SOIDWORKS Electrical中统计槽满率的经验技巧

近期有一些客户咨询&#xff0c;为什么在SOLIDWORKS Electrical 3D 中做完3D布线工作&#xff0c;但是在统计线槽槽满率的时候不能正常计算。因此我们总结了以下几点经验。 一、对于SOLIDWORKS Electrical中的计算线槽率的功能&#xff0c;除了所使用的线槽需要满足两个条件&am…

【Unity服务器01】之【AssetBundle上传加载u3d模型】

首先打开一个项目导入一个简单的场景 导入怪物资源&#xff0c; AssetBundle知识点&#xff1a; 1.指定资源的AssetBundle属性标签 &#xff08;1&#xff09;找到AssetBundle属性标签 &#xff08;2&#xff09;A标签 代表&#xff1a;资源目录&#xff08;决定打包之后在哪…

LDO电源模块如何快速设计布局

在现代电子设备遍布的时代&#xff0c;电源模块的设计与应用成为了电子工程领域中的核心议题。而LDO&#xff08;低压差线性稳压器&#xff09;电源模块&#xff0c;因其出色的线性特性和稳定性&#xff0c;在众多应用中备受青睐。为了满足不断增长的电子设备性能需求&#xff…

控价服务商的选择标准

品牌控价旨在对渠道进行有效管控&#xff0c;维护品牌自身价值以及经销商的合法权益&#xff0c;同时也为消费者提供稳定的购物价格。在这一过程中&#xff0c;不但要对线上价格进行把控&#xff0c;线下价格同样需要品牌投入精力去管理。就线上而言&#xff0c;由于链接数量众…

面向对象的进阶---static

1.static 静态变量 package com.itheima.a01staticdemo01;public class Student {private String name;private int age;public static String teacherName;public Student() {}public Student(String name, int age) {this.name name;this.age age;}/*** 获取* return n…

基于单片机的智能台灯控制系统

摘要&#xff1a; 文章设计一款单片机智能台灯控制系统&#xff0c;实现对台灯的手动和自动控制功能&#xff0c;以 STC89C52 单片机作为多功能智能台灯的主控制器&#xff0c;光电检测模块检测坐姿&#xff0c;红外传感器检测人体&#xff0c;光敏电阻检测光强&#xff0c;同…

找不到x3daudio1_7.dll无法运行的原因分析及6种解决方法

当您遇到软件或游戏中提示“x3daudio1_7.dll丢失”的问题时&#xff0c;通常意味着您的系统中缺少这个特定的动态链接库文件。x3daudio1_7.dll 是微软DirectX的一部分&#xff0c;找不到x3daudio1_7.dll会导致软件游戏无法启动运行&#xff0c;下面小编就分享几种靠谱的解决方法…

msvcp120.dll丢失的解决方法,总结几种有效的解决方法

最近&#xff0c;我在使用计算机时遇到了一个问题&#xff0c;系统提示我丢失了msvcp120.dll文件。这让我感到非常困扰&#xff0c;因为这个问题导致我无法正常运行一些程序。经过一番搜索和尝试&#xff0c;我找到了几种修复这个问题的方法&#xff0c;并成功解决了这个问题。…