Linux操作系统 - 进程控制

目录

进程创建

进程退出

进程等待

进程替换


进程创建

在操作系统中,除了系统启动之后的第一个进程(根进程,1号进程)由系统来创建外,其余进程都必须由已存在的进程来创建。其中,这个新创建的进程叫做子进程,而创建子进程的进程叫做父进程。其中,根进程是Linux中所有进程的祖宗,其余进程都是根进程的子孙。所有命令行下执行的指令都是 shell/bash 的子进程。具体如下所示:

图片出处: Linux进程的创建与管理-CSDN博客

而在Linux下我们通常用一个fork系统调用函数来为当前进程创建一个子进程。fork函数对子进程的返回值为0,对父进程的返回值是子进程的pid。特别的,如果创建子进程失败则返回-1。父进程不仅负责创建子进程,还负责对子进程的资源进行回收处理,而且父进程只对子进程负责,不对孙子进程负责。至于fork之后生成的子进程与其父进程谁先执行谁后执行是不确定的,这与操作系统的进程调度策略有关,不能一概而论。

那么新进程(子进程)如何被创建出来的呢?一个新进程的创建大致可以概括为如下过程:

  1. 为新进程分配一个进程标识符PID。
  2. 在内核中为其创建并分配一个PCB。
  3. 复制父进程的进程上下文,即将父进程PCB中的大部分内容拷贝覆盖到子进程,但像PID这种内容是不会进行覆盖的。
  4. 创建页表,并复制父进程的虚拟地址空间内容(包括命令行参数和环境变量等信息)与页表映射关系等内容,并将页表权限都设为只读。 

其中,上述的这些过程是在fork函数内部完成的。也就是说,在拿到fork的返回值之前,上述的这些工作就已经完成了。

所以,子进程在创建初期并不会真的为其在物理内存中分配内存,而是用的一种“写时拷贝”的技术。子进程起初与父进程共享代码与数据, 当父进程或子进程试图对某一块区域的数据进行修改时,就会触发缺页中断,然后才会为子进程真正的分配一块物理内存,此时父子进程的这块区域的内容才是真正的独立开来了。

需要注意的是,并不是只有数据段的内容会发生写时拷贝,在进程替换的过程中,代码段的内容也会发生写时拷贝,父子进程的代码段内容独立开来,不再共享。


相关拓展 - vfork函数

Linux下除了fork函数可以创建一个进程,还有一个不常用的vfork函数也可以创建进程。vfrok函数与fork基本上类似,不同之处在于fork创建子进程之后父子进程的执行顺序是不一定的,而vfork创建子进程之后,是保证子进程先运行,在子进程调用exec(进程替换)或exit(退出进程)之后父进程才可能被调度运行。所以,用vfork创建进程时,子进程里一定要调用exec或exit,否则程序会出问题,没有意义。

进程退出

之前我们在编写C/C++程序时,一般会在main函数的最后加一个return 0,至于这个return 0是干什么的我们那时还不知道,也许有人曾经尝试过return不为0的值,好像对最后形成的那个可执行程序并没有什么影响。

其实,main函数的返回值就是当前程序的退出码,我们可以利用这个退出码分析当前进程的退出状态,一般我们可以将一个进程的退出场景分为如下三类:

  • 正常终止,代码运行结果正确
  • 正常终止,代码运行结果不正确
  • 异常终止,与信号有关

进程的异常终止,本质上是进程收到了某种信号(比如 kill -9),导致代码发生异常,立即终止。

而前两者正常终止的情况则可以利用进程的退出码来分析。如果退出码为0,就表示正常终止,且代码的运行结果正确。如果退出码不为0,就表示进程虽然正常终止,但代码的运行结果不正确,此时我们就需要根据不同的退出码来分析问题了。如果退出码是用的语言内嵌的像errno等变量,可以通过相关的函数打印或者查看对应的信息,如果进程的退出码是我们自己设的,就要根据实际情况来分析了。其中,可以用 echo $? 输出最近一次进程退出时的退出码。

所以,判断一个进程运行的怎么样,归根结底可以通过两个数字来判断:信号的数字,退出码的数字。也就是说,父进程只需要监视子进程的信号信息和退出码即可

在main函数中我们可以用return返回一个退出码说明当前进程的执行结果如何。例如我们可以在打开一个文件时判断是否打开成功,如果打开失败则可以直接返回一个退出码:

int main()
{
    FILE* fp = open("myfile.txt", "r");
    if(fp == NULL) // 打开文件失败,直接返回
    {
        // errno,C语言自己维护的一个错误码, 可以通过相关函数查询或打印对应的错误信息
        return errno; 
    }
    return 0;
}

而真正用来终止退出一个进程的其实是exit和_exit等函数。exit和_exit都是直接终止程序,并向上层返回一个退出码。exit和_exit的一个很明显的区别是,exit在终止程序时,会强制刷新缓冲区中的内容,而_exit只是单纯的退出进程,并不会刷新缓冲区的内容。

如果不知道什么是缓冲区,可以参考如下概念

缓冲区的概念:

        在Linux的标准函数库中,有一种被称为"缓冲I/O"的操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区当中读取。同样的,每次写文件的时候,也仅仅是写入内存的 缓冲区,等满足了一定的条件时(如达到一定数量或遇到特定字符等,最典型的就是咱们的vim中使用的 : w命令),再将缓冲区中的内容一次性写入文件。这种技术大大增加了文件读写的速度,但是也给编程带来了一点麻烦。比如有些数据你认为已经被写入到文件中,实际上因为没有满足特定的条件,他们还只是被保存在缓冲区内,这时用_exit()函数直接将进程关闭掉,缓冲区中的数据就会丢失。因此,为了数据的完整性,请使用exit()函数

究其原因就是,exit是C语言封装好的一个函数,而_exit是Linux下的一个系统调用。而其实C语言从exit函数底层上就是封装了_exit函数。所以,其实C语言代码中的缓冲区,是由C语言来维护的,Linux操作系统并不知道这个缓冲区的存在,故而_exit不会执行C语言缓冲区的清理操作。

所以,其实return并没有终止进程的作用,return只是用于终止一个函数,而main函数的返回值之所以就是退出码,是因为一个进程的执行是按照main函数的代码逻辑来的,在main函数结束时,会隐式地调用exit函数。所以,执行进程退出操作的本质上还是exit这一类的函数。

除了exit,C语言中还有一个abort函数用来终止一个程序。

与exit之间的区别是,exit是正常退出一个程序,而abort则是异常终止程序。

进程等待

我们知道,当父进程负责管理子进程,既负责子进程的创建工作,也负责子进程的资源回收以及处理结果的接收。这里的子进程资源回收指的是解决僵尸进程及其带来的内存泄露问题。而父进程创建子进程一般是为了让子进程来完成某些任务,所以就需要获取子进程的退出信息,进而就能知道子进程的任务完成的如何。那么这些后续的善后工作,就是通过进程等待的来做的。其中,让父进程对子进程资源进行回收的等待过程就叫做进程等待

Linux下,通常是通过 wait/waitpid 这两个系统调用函数进行进程等待的。

我们以waitpid为例来介绍一下进程等待,waitpid搞清楚了之后,wait也就不难了。

首先来介绍返回值:返回值大于0时表示等待成功,且子进程已退出,此时返回的就是子进程的PID。返回值为-1就表示等待失败,并会设置errno变量的值。而当返回值等于0的时候,表示等待成功,但子进程目前还没有退出(非阻塞等待的情况)。

第一个参数(pid)表示要等待的进程PID,当pid大于0时,就表示等待指定PID的进程,如果pid是-1,就表示随机等待一个进程,即哪个进程先退出,他就等待哪个进程。

而第三个参数options表示等待的方式,当options为0时表示为阻塞式等待,options为WNOHANG (一个宏)时,表示为非阻塞等待。阻塞等待是指,父进程将一直等待子进程的退出,期间不会进行任何其它的活动。与硬件阻塞类似,阻塞等待时就是把父进程的PCB链入到子进程的等待队列中(随之把父进程的状态设为s),当子进程退出时,操作系统再把父进程从子进程的等待队列中拿出来,放到运行队列再继续运行。而非阻塞等待是指,等待的过程中并不会一直在那里等待,而是得到等待结果之后立即返回。所以通常非阻塞式等待还要配合循环轮询使用。好处就是我们在等待的过程中可以做一些占用时间不多的小事情。

最后第二个参数status是一个int*的指针,这是一个输出型参数,表示进程等待之后,将这个status设置为一种特殊格式的数据。我们知道一个int型数据占4个字节32位,这里我们只讨论status的低16位。我们不能对status整体使用,只能截取其部分内容,以获取我们想要得到的信息。
如果被等待进程是正常退出,那么其低8位(0-7位)全部为0,8-15位的内容才是的我们想拿到的退出码。而如果是异常退出(被信号所杀),那么status的高8位(8-15位)的内容是未使用区域,内容是未知的,我们也不关心,而其低7位(0-6)位的内容就是终止信号对应的代码,其第8位(7)的内容是core dump标志,至于什么是core dump,可以参考下面的概念。

core dump的概念:

        Core Dump 也称之为“核心转储”, 若当前操作系统开启了 core dump ,当程序运行过程中发生异常或接收到某些信号使得程序进程异常退出时, 由操作系统把程序当前的内存状况以及相关的进程状态信息存储在一个 Core 文件中, 即 Core Dump 。通常,Linux 中如果内存越界会收到 SIGSEGV 信号,然后就会进行 Core Dump 相关操作。

由于status的规格比较复杂,所以要获取某些值的话就要用到一些麻烦的位运算,所以其对应的头文件中为我们提供了一些宏,以简便我们的操作,如下是部分常用操作。

WIFEXITED(status):如果子进程正常终止,则返回true

WEXITSTATUS(status):返回子进程的退出码。只有当WIFEXITED返回true时才能使用。

WIFSIGNALED(status):如果子进程被信号终止,则返回true。

WTERMSIG(status):返回导致子进程终止的信号值。只有当WIFSIGNALED返回true时才能使用。

WCOREDUMP(status):如果子进程产生了core dump,则返回true。只有当WIFSIGNALED返回true时才能使用。


而wait则要相对简单一些,wait只有一个status参数,只能以阻塞式等待的方式进行进程等待,也不能指定等待的进程PID,只能随机等待一个最先退出的进程。

其它注意事项如下

  1. wait/waitpid 在等待成功且子进程退出的情况下,会自动对子进程进行回收。
  2. 一般而言,父子进程谁先运行不一定,但一般都是父进程后退出。因为父进程需要回收其子进程资源。进而我们能知道,多进程的多执行流,是由父进程发起,最后由父进程来统一回收的。
  3. 对于异常退出的情况,可以通过kill -l指令查看退出码所对应的信号信息。
  4. 当一个进程异常退出时,其退出码就已经没有意义了。
  5. 父进程是没法直接获取到子进程的数据的,所以需要通过wait等系统调用来获取子进程的退出信息。
  6. 父进程是如何获取子进程退出信息的,即wait和waitpid在系统层面是怎么做到的呢?当子进程在退出时,会将子进程的退出码、所接收的信号等一系列推出信息写入到其PCB中(状态变成z,并释放内存资源,变成僵尸状态)。所以wai/waitpid的底层的工作就是将退出信号和退出码等信息整合成一个status,向上返回给父进程(同时将子进程PCB的状态由z改为x)

进程替换

进程替换,严格来讲应该叫进程程序替换,并不是指的替换进程,而是指的替换进程中的程序内容。是指将当前进程的程序内容替换为另一个程序的内容。比如当我们创建一个子进程后,有时会让子进程执行当前代码的内容,但有时也需要子进程去执行另一个程序的内容,这时就需要用到进程替换,将当前进程的程序代码替换为另一个程序的。

在Linux下,操作系统为我们提供了一系列的进程替换函数,俗称exec函数簇,它们分别是:

  1. int execl(const char *path, const char *arg, ...);
  2. int execlp(const char *file, const char *arg, ...);
  3. int execle(const char *path, const char *arg, ...,char *const envp[]);
  4. int execv(const char *path, char *const argv[]);
  5. int execvp(const char *file, char *const argv[]);
  6. int execve(const char *filename, char *const argv[], char *const envp[]);
  7. int execve(const char *filename, char *const argv[], char *const envp[]);

这些函数看起来很混乱,但其实只要掌握了规律就很好记了。

首先,这些函数的第一个参数通常为程序的所在路径(精确到具体的文件名),剩下的参数表示传入的命令行参数或是环境变量等信息。也就是说,先找到程序所在位置,再执行对应的程序。

而exec后续的字母,也是有规律的:

  1. l - list,表示命令行参数的传入采用列表传参的方式(仅限于命令行参数,环境变量的内容依旧是采用数组传递),比如printf的传参。arg为程序名,后续部分为执行参数,且必须以NULL结尾。
  2. v - vector,表示命令行参数以数组的形式传参。其中,数组的0号元素表示程序名,后续数组元素为执行程序时的参数,argv的最后以NULL结尾。
  3. p - path,表示这个函数会自动到环境变量PATH中根据file(第一个参数)去寻找对应的程序。所以带p的第一个参数不用写路径信息,只需要写文件名就可以了。也就是说,带p的比较适合用Linux内部的指令的进程替换。
  4. e - env,表示可以传入环境变量(都是以数组的形式传入),其中envp数组的最后一个元素必须为NULL以表示结束。

其中,前六个函数是统一在3号手册中的,最后一个函数单独在2号手册中,那么为什么 execve 这么特殊呢?这是因为上述的那些3号手册的那函数,其实底层都是封装的这个execve。也就是说,其实最终被调用的也就只有execve这一个。那么既然已经有了execve,还要有这些其它的函数呢?其实主要还是为了满足各种调用的场景,简化操作。

进程替换的原理

进程替换就是,将替换进程在磁盘中的代码和数据在当前进程对应的内存中的代码和数据内容覆盖掉。进程覆盖对进程的整体影响并不大,仅执行一些例如重新建立页表映射和重置虚拟地址空间内容等操作。而对于进程的绝大部分属性,比如PCB、PID、优先级等等都是保持原状,不会改变。

也就是说,在进程替换的过程中并没有创建新进程,只是替换程序的代码和数据放到当前进程的壳子当中,进程的本质属性并不受影响。

注意,如果此时代码段的内容还是处于父子进程的共享状态,那么替换程序对代码内容进行覆盖时,也会触发缺页中断,进而发生写时拷贝。

注意事项

  1. 可以认为命令行怎么撰写指令,我们的arg/argv就按照什么样的顺序撰写。其中,在执行自定义程序时,arg等内容也是可以不用指明路径的,因为第一个参数已经为我们指明路径了。
  2. 一个小技巧,exec簇中带p的,第一个参数是直接传argv[0]的。
  3. argv和envp这两个命令行参数和环境变量的数组必须以NULL结尾,以表示结束。
  4. exec簇的健壮性很强,有些小错误是不影响的,比如第二个参数写错,但第一个参数路径没错,这种情况是可以正常跑通的。但最好还是按标准的写法来。
  5. 除了可以将父进程默认的环境变量表传递给子进程外,exec簇的函数还可以将父进程自定义的一张环境变量表传递给子进程,以替换默认的那张环境变量表。即传递的环境变量表不是新增的,而是覆盖式的。
  6. 进程替换如果成功了,后续部分的代码是不会被执行的。因为内存中的代码和数据已经被替换了,执行的都已经不是当前的这份代码了。也就是说,如果进程替换成功,就不会执行原来的内容了。如果进程替换失败,就会继续执行剩下的内容。
  7. exec簇等函数只有失败的返回值(即-1),因为调用成功之后是不会再执行后续内容的。所以不用判断返回值,只要继续执行后续代码,就是程序替换失败了。
  8. 程序替换不单能替换C/C++语言的程序,还能替换其他所有语言程序,比如shell、Python、Java等等。这是因为exec簇等函数叫做进程替换,只要是个进程就能进行替换。也就是说,系统大于一切进程。
  9. 子进程的环境变量是不会影响其父进程的。但父进程的环境变量能够被子进程继承。也就是说,环境变量被子进程继承下去是一种默认行为,不受程序替换的影响。
  10. 那么为什么程序替换之后,环境变量也不变呢?这是因为,程序替换只会替换新程序的代码和数据。命令行参数环境变量等一系列信息不会被替换。也就是说,main函数的命令行参数和环境变量等信息是可以通过程序替换的方式,交付给子进程的。

内容补充

  1. ELF头有一个entry字段,记录的是可执行程序的入口地址。程序计数器,又叫pc、eip,是CPU中的一个寄存器,存放的是下一条指令的地址。不同的进程都有单独的eip信息,所以程序在执行过程中会不断更新eip的内容。那么exec簇中的这些的函数只需要将可执行程序的entry字段填到子进程的eip中,就能使得每次程序替换之后都可以从新程序的起始位置开始执行。
  2. 所以,bash/shell下执行的进程在创建时,是进程部分的PCB等一些内核数据结构先被创建,然后再通过进程替换操作将程序的内容和数据加载到内存。也就是说,是先创建的内核数据结构,然后再将代码和数据加载到内存的。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Nginx(四) absolute_redirect、server_name_in_redirect、port_in_redirect 请求重定向指令组合测试

本篇文章主要用来测试absolute_redirect、server_name_in_redirect和port_in_redirect三个指令对Nginx请求重定向的影响,Nginx配置详解请参考另一篇文章 Nginx(三) 配置文件详解 接下来,在Chrome无痕模式下进行测试。 测试1:absolute_redi…

微软Surface/Surface pro笔记本电脑进入bios界面

微软Surface笔记本电脑进入bios界面 方法一推薦這種方法:Surface laptop 进BIOS步骤 开机后,不停按音量键进bios界面。 方法二:Surface Book、Surface Pro进bios步骤 1、关闭Surface,然后等待大约10秒钟以确保其处于关闭状态。…

百度智能小程序源码系统:打造极致用户体验的关键 带完整搭建教程

大家好啊,今天罗峰来给大家分享一款百度智能小程序系统源码。一起来看看吧。 百度智能小程序源码系统是百度从做智能小程序的第一天开始就致力于打造真正开源开放的生态的产物。作为目前业内唯一真正开源的平台,百度智能小程序将开放性放在重要位置&…

大数据毕业设计选题推荐-机房信息大数据平台-Hadoop-Spark-Hive

✨作者主页:IT研究室✨ 个人简介:曾从事计算机专业培训教学,擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

win10查看wifi密码

文章目录 标题win10查看wifi密码命令方式窗口 标题win10查看wifi密码 命令方式 # name 为指定的wifi名称 netsh wlan show profiles name"TP-LINK_1946" keyclear窗口

春秋云境靶场CVE-2021-41402漏洞复现(任意代码执行漏洞)

文章目录 前言一、CVE-2021-41402描述二、CVE-2021-41402漏洞复现1、信息收集1、方法一弱口令bp爆破2、方法二7kb扫路径,后弱口令爆破 2、找可能可以进行任意php代码执行的地方3、漏洞利用找flag 总结 前言 此文章只用于学习和反思巩固渗透测试知识,禁止…

pwnable.kr--pwn游戏之fd

题目描述: 大致告诉我们研究的可能是Linux下的文件描述符。需要我们用ssh链接过去找到flag。于是我们就过去看看: 乍看情况有点像简单nc,我们尝试看看目录下都有什么: 看到flag,那么尝试输出呢? Permission…

全新的FL studio21.2版支持原生中文FL studio2024官方破解版

FL studio2024官方破解版是一款非常专业的音频编辑制作软件。目前它的版本来到了全新的FL studio21.2版,支持原生中文,全面升级的EQ、母带压线器等功能,让你操作起来更加方便,该版本经过破解处理,用户可永久免费使用&a…

【DevOps】Git 图文详解(一):简介及基础概念

Git 图文详解(一):简介及基础概念 1.简介:认识 Git2.基础概念:Git 是干什么的?2.1 概念汇总2.2 工作区 / 暂存区 / 仓库2.3 Git 基本流程2.4 Git 状态 1.简介:认识 Git Git 是当前最先进、最主…

Python---列表 集合 字典 推导式(本文以 字典 为主)

推导式: 推导式comprehensions(又称解析式),是Python的一种独有特性。推导式是可以从一个数据序列构建另一个新的数据序列(一个有规律的列表或控制一个有规律列表)的结构体。 共有三种推导:列表…

【漏洞复现】OneThink前台注入漏洞

漏洞描述 OneThink 是一个基于 PHP 的开源内容管理框架,旨在简化和加速Web应用程序的开发过程。它提供了一系列通用的模块和功能,使开发者能够更轻松地构建具有灵活性和可扩展性的内容管理系统(CMS)和其他Web应用。 免责声明 …

RS485接线方式

用2个触点连接RS485设备——RS485引脚半双工分配: 用4个触点连接RS485设备——RS485引脚全双工分配: 参考文章:RS485引脚说明及接口说明 文章目录 RS485 接线方式引言RS485通信标准简介基本特性差分信号:RS485使用差分信号传输&am…

AOF是什么?

目录 一、AOF是什么? 二、使用AOF 三、命令写入 四、重写机制 4.1 触发AOF 4.2 AOF执行流程 一、AOF是什么? AOF是Append Only File,是Redis中实现持久化的一种方式。以独⽴⽇志的⽅式记录每次命令,重启时再重新执⾏ AOF ⽂件中的…

Day32力扣打卡

打卡记录 买卖股票的最佳时机 IV(状态机DP) 链接 class Solution:def maxProfit(self, k: int, prices: List[int]) -> int:n len(prices)max lambda x, y: x if x > y else yf [[-0x3f3f3f3f] * 2 for _ in range(k 2)]for i in range(k 2…

Vim 从何而来?

Vim 编辑器的创造者、维护者和终身领导者 Bram Moolenaar 为了纪念这位杰出的荷兰程序员,我们今天来聊一聊 Vim 的历史。 Vim 无处不在。它被很多人使用。同时 Vim 可能是世界上 “最难用的软件之一” ,但是又多次被程序员们评价为 最受欢迎的 代码编辑…

设计模式—结构型模式之外观模式(门面模式)

设计模式—结构型模式之外观模式(门面模式) 外观(Facade)模式又叫作门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。 例子 我们的电脑会有很多 组件&am…

搜维尔科技:业内普遍选择Varjo头显作为医疗VR/AR/XR解决方案

Varjo 的人眼分辨率混合现实和虚拟现实头显将医疗专业人员的注意力和情感投入提升到更高水平。借助逼真的 XR/VR,医疗和保健人员可以为最具挑战性的现实场景做好准备! 在虚拟、增强和混合现实中进行最高水平的训练和表现 以逼真的 3D 方式可视化医疗数据…

JS原生-弹框+阿里巴巴矢量图

效果&#xff1a; 代码&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content&q…

遵循开源软件安全路线图

毫无疑问&#xff0c;开源软件对于满足联邦任务所需的开发和创新至关重要&#xff0c;因此其安全性至关重要。 OSS&#xff08;运营支持系统&#xff09; 支持联邦政府内的每个关键基础设施部门。 联邦政府认识到这一点&#xff0c;并正在采取措施优先考虑 OSS 安全&#xff…

Python框架篇(2):FastApi-参数接收和验证

提示: 如果想获取文章中具体的代码信息&#xff0c;可在微信搜索【猿码记】回复 【fastapi】即可。 1.参数接收 1.1 路径参数(不推荐) 1.代码清单 在app/router下&#xff0c;新增demo_router.py文件,内容如下: from fastapi import APIRouterrouter APIRouter( prefix&qu…