Linux系统编程 day05 进程控制

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); // 获取当前运行进程的pidpid_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(); // 父亲的pidfor(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.mzph.cn/news/172532.shtml

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

相关文章

FO-like Transformation

参考文献&#xff1a; [RS91] Rackoff C, Simon D R. Non-interactive zero-knowledge proof of knowledge and chosen ciphertext attack[C]//Annual international cryptology conference. Berlin, Heidelberg: Springer Berlin Heidelberg, 1991: 433-444.[BR93] Bellare M…

软件介绍01- koodo Reader支持所有电脑平台!

1 软件简介 Koodo Reader软件是一款阅读器&#xff0c;可以阅读各种格式的文档。用来代替kindle。界面简洁&#xff0c;好看&#xff0c;阅读功能强大&#xff0c;而且可以多设备同步。 因为开源&#xff0c;所以免费。而且支持所有电脑平台&#xff01; 支持格式&#xff1a…

Android修行手册-ViewPager定制页面切换以及实现原理剖析

Unity3D特效百例案例项目实战源码Android-Unity实战问题汇总游戏脚本-辅助自动化Android控件全解手册再战Android系列Scratch编程案例软考全系列Unity3D学习专栏蓝桥系列ChatGPT和AIGC &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分…

kafka的设计原理

文章目录 1 Kafka简介2 Kafka的架构2.1 Kafka 一些重要概念2.2 工作流程2.3 副本原理2.4 分区和主题的关系2.5 生产者2.5.1 分区可以水平扩展2.5.2 分区策略 2.6 消费者2.6.1 消费方式2.6.2 分区分配策略 2.7 数据可靠性保证2.7.1 副本数据同步策略2.7.2 ACK 应答机制2.7.3 可靠…

Java抽象类和接口(1)

&#x1f435;本篇文章将对抽象类和接口相关知识进行讲解 一、抽象类 先来看下面的代码&#xff1a; class Shape {public void draw() {System.out.println("画");} } class Cycle extends Shape {public void draw() {System.out.println("圆形");} } …

开发知识点-ArkTS-鸿蒙开发-Typescript

Typescript IED IED https://developer.harmonyos.com/cn/develop/deveco-studio/#download

打开CMD的六种方法,CMD快捷键,CMD命令大全及详解

目录 前言1. winR快捷键2、通过文本文档创建&#xff1b;3、通过C盘中的cmd.exe文件打开&#xff1b;4、创建快捷方式&#xff1b;5、通过PowerShell打开&#xff1b;6、通过文件夹导航栏打开&#xff1b; 前言 自己的电脑win键失灵了&#xff0c;想通过winR来调出cmd&#xff…

【Linux基础】Linux常见指令总结及周边小知识

前言 Linux系统编程的学习我们将要开始了&#xff0c;学习它我们不得不谈谈它的版本发布是怎样的&#xff0c;谈它的版本发布就不得不说说unix。下面是unix发展史是我在百度百科了解的 Unix发展史 UNIX系统是一个分时系统。最早的UNIX系统于1970年问世。此前&#xff0c;只有…

Doris单机部署——2.0.1.1版本

目录 一、前期准备工作 1.设置系统最大文件打开句柄数 2.时钟同步 3.关闭每台机器的交换分区 4.下载安装包 二、单节点部署安装Doris (一)安装fe 1.解压改名 2.修改配置文件 3.创建元数据目录 4.启动fe 5.访问fe的webUI (二)安装be 1.进入be目录下&#xff0c;修…

Leetcode—35.搜索插入位置【简单】

2023每日刷题&#xff08;四十&#xff09; Leetcode—35.搜索插入位置 实现代码 int lower_bound(int* arr, int numsSize, int tar) {int left 0, right numsSize;int mid;// 左闭右开[left, right)while(left < right) {mid left (right - left) / 2;if(arr[mid] &…

Cadence Vmanager vsif文件编写指南(持续更新...)

目录 1.NTF格式介绍 1.1.1 {属性&#xff1a;值}定义 1.1.2类别 1.1.3语法 2.vsif文件中有效的container 2.1 session {…} 1.NTF格式介绍 Cadence的Vmanager工具采用vsif类型的文件作为regression的输入文件&#xff0c;采用vplanx/csv类型的文件作为vplan的输入文件&am…

BC77 简单计算器(牛客)

#include <stdio.h> int main() {double a, b, d;//用来接收浮点数char c;//用来接受符号scanf("%lf %c %lf", &a, &c, &b);if (c || c - || c * || c /)//判断输入的运算符号不包括在&#xff08;、-、*、/&#xff09;范围内{switch (c)//根…

Kotlin应用——使用kt进行web开发 使用h2database进行初始化数据库 mybatis-plus使用

Kotlin 是一门现代但已成熟的编程语言&#xff0c;旨在让开发人员更幸福快乐。 它简洁、安全、可与 Java 及其他语言互操作&#xff0c;并提供了多种方式在多个平台间复用代码&#xff0c;以实现高效编程。 kt入门的合集文章如下&#xff1a; Kotlin学习——kt入门合集博客 &…

P16 C++构造函数

目录 前言 01 什么是构造函数呢&#xff1f; 02 非构造函数初始化变量 03 构造函数初始化变量 04 带参数的构造函数。 最后的话 前言 我们继续学习 C 的面向对象编程&#xff0c;本章主要是讲其中的 构造函数。 01 什么是构造函数呢&#xff1f; 构造函数基本上是一种特…

tinyViT论文笔记

论文&#xff1a;https://arxiv.org/abs/2207.10666 GitHub&#xff1a;https://github.com/microsoft/Cream/tree/main/TinyViT 摘要 在计算机视觉任务中&#xff0c;视觉ViT由于其优秀的模型能力已经引起了极大关注。但是&#xff0c;由于大多数ViT模型的参数量巨大&#x…

MetaObject-BeanWrapper-MetaClass-Reflector的关系

MetaObject、BeanWrapper、MetaClass、Reflector之间是通过装饰器模式逐层进行装饰的。其中MetaObject、BeanWrapper是操作对象&#xff1b;MetaClass、Reflector是操作Class ObjectWrapper类结构图 BaseWrapper是对BeanWrapper、MapWrapper公共方法的提取及类图的优化&#…

线程的创建方式

作者简介&#xff1a; zoro-1&#xff0c;目前大二&#xff0c;正在学习Java&#xff0c;数据结构&#xff0c;mysql&#xff0c;javaee等 作者主页&#xff1a; zoro-1的主页 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01;&#x1f496;&#x1f496; 线程的创建方…

知识点小总结

‘Integer(int)‘ 已经过时了 https://blog.csdn.net/qq_43116031/article/details/127793512 解决Java中的“找不到符号“错误 解决Java中的“找不到符号“错误_java: 找不到符号_很酷的站长的博客-CSDN博客 可右键打开 错误: 编码 UTF-8 的不可映射字符 错误: 编码 UTF-8 …

Less的函数的介绍

文章目录 前言描述style.less输出后言 前言 hello world欢迎来到前端的新世界 &#x1f61c;当前文章系列专栏&#xff1a;Sass和Less &#x1f431;‍&#x1f453;博主在前端领域还有很多知识和技术需要掌握&#xff0c;正在不断努力填补技术短板。(如果出现错误&#xff0c;…

什么是无监督学习

1 概况 1.1 定义 无监督学习&#xff08;Unsupervised Learning&#xff09;是机器学习的一种类型&#xff0c;它涉及从未标记的数据中发现隐藏的模式。与监督学习不同&#xff0c;无监督学习的数据没有显式的标签或已知的结果变量。其核心目的是探索数据的内在结构和关系。无…