Linux_17进程控制

ops/2025/3/13 17:18:04/

前提回顾:

页表可以将无序的物理地址映射为有序的;

通过进程地址空间,避免将内存直接暴漏给操作系统;

cr3寄存器存放的有当前运行进程的页表的物理地址;

一、查看命令行参数和环境变量的地址

因为命令行参数和环境变量都是字符串的形式,所以这里我们可以通过字符串直接打印地址;

通过验证最后可以知道:命令行参数和环境变量位于栈区的上方! 

(子进程继承父进程的环境变量 --- 因为进程地址空间存放的有!)

代码是不可写入的(无论是父进程还是子进程,映射到代码区,页表对应的权限关系是只读的!)

操作系统怎么识别到父子共享的数据块,此时需要进行写时拷贝呢?

        如果我们要对当前的父进程的数据进行写入的时候,无论数据是只读还是可写,系统都会将其改为只读权限!(子进程也是只读),此时如果想写入就会触发系统权限的问题(不做异常处理),此时操作系统会对要访问的区域进行检查,如果权限是可写的就会进行写时拷贝!

通过fork创建多个进程

下面我们通过循环创建多个线程:

父子进程(或兄弟进程)被创建出来后,谁先运行不可知!(由调度器决定!)

 进程退出的场景都有哪些?

  • 代码运行完毕,结果正确;
  • 代码运行完毕,结果不正确;
  • 代码异常终止;

进程中,一般谁会关心我们的运行情况呢?(--- 父进程!)

相比于正确的结果,我们一般关心的是为什么程序运行会出错?

在C语言中,可以用return的不同返回值数字,表征不同的出错原因(即退出码);

即:main函数的返回值得到本质表示的是:进程运行时是否是正确的结果,如果不是,可以用不同的数字来表示不同的出错原因!

使用 $? 可以获取上一个进程运行结束后的退出码(最近一个进程运行的退出码);

这里第一次执行查看的时./myproc的进程码,接下来几次查看的都是echo的进程码!

错误码0、1...都是给计算机查看的,人自己查看可以通过Linux提供的接口将其转化为字符串(需要包含string.h这个头文件);

这里当我们进行ls查看一个不存在的文件的时候,此时我们使用echo $? 打印的结果正好是错误代码中2!

我们也可以自定义错误码,而不是一定非要根据系统的体系走;

此时返回错误码对应的自定义的下标即可! 

C语言有个errno全局变量!(用于保存最近一次执行的错误码!)

当我们调用全局函数失败的时候,此时errno会返回对应的错误码!

此时我们可以打印errno对应的错误代码,并将其返回给父进程,让父进程直到此时调用有问题;

代码异常终止,我们可以认为:代码可能没有跑完(如果程序在main返回之前异常终止,此时我们认为该错误码没有意义,不关心退出码!);

进程的控制是由信号控制的:

我们可以通过kill -l对对应的进程进行控制;

结论:进程出现异常,本质上就是进程收到了对应的信号! 

这里exit()在任何地方被调用后,都表示调用该进程直接退出!(不会执行后面的打印语句)

此时如果我们进行return,函数会直接返回,然后在main里面继续打印printf的内容!

_exit()这是一个系统调用接口;exit是库函数!

exit()实际上是先将对应的缓存区和自定义的函数清理,然后再调用_exit() ;

此时我们提出一个问题:这个缓存区应该在内存的那个位置?

一定不在内核区,如果在内核区的话此时_exit()也会刷新缓存流!

二、进程等待

我们要通过等待进程,获取紫禁城的退出情况 --- 直到我们给子进程布置的任务完成的怎么样,可以关心也可以不关系(不关心就设置为NULL)

异常实质上就是信号!(运行中报错)

如果我们对应的wait在调用的时候,此时子进程一直在执行工作,没有退出;

那么此时父进程就会阻塞等待子进程!(阻塞状态不仅发生在等待硬件 - 键盘、也会发生在等待软件 - 进程)

输出型参数:在函数调用的时候,由函数内部修改其值并返回调用者的参数(例如malloc)

status就是一个输出型参数!

父进程要拿子进程的状态数据,为什么必须要用wait等系统调用呢?

进程具有独立性!如果我们全局变量,此时子进程将这个全局变量进行修改,无法传递给父进程!(必须得通过系统调用来实现!)

前8位表示的是终止信号,(例如我们勇敢什么特殊信号杀掉进程);

第八位为标志位,此时我们暂不关心;

8~15表示的是退出状态(即子进程的退出状态码);

 

这里父进程在对应的数据和代码里面,通过系统调用接口,查询子进程的退出信息(exit_code, exit_signal),其中这两个主要的信息包含在tast_struct里面!

因此waitpid函数的本质是:读取子进程的task_struct的内核数据结构对象;

等待子进程返回信息的时候,什么时候会等待失败呢?

当等待的子进程不是对应的子进程!此时返回-1;

  • WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) --- 本质上检查的是信号位(也就是status & 0x7f --- > 取小的七位!)
  • WEXITSTATUS(status): WIFEXITED非零,提取子进程退出码。(查看进程的退出码)也就是(status>>8)& 0xFF
用法如下所示:

waitpid中的option选项:

  • 当此时为0的时候,即为阻塞等待方式;
  • WNOHANG(wait no 夯住了) --- 非阻塞轮询!

阻塞等待方式:子进程此时一直为R状态运行,父进程需要等待,把父进程投递在等待队列中,

(我们可以认为子进程维护的PCB里面也维护了一个等待队列!--- 为等待该进程的使用的)

引入非阻塞轮询的概念:

阻塞等待的时候,此时父进程只能等待,而不能先进行一会自己的工作;

        但是非阻塞轮询此时会一直询问子进程的状态,如果没准备好,此时父进程会先忙自己的工作,过段时间再进行询问;

对于非阻塞轮询有三种返回条件:

  • ret_pid>0(也就是对应子进程的PID) | ret_pid <0,此时表示此时等待的条件已经就绪或者失败!
  • ret_pid = 0 表示还没有继续,此时会继续等待;

为了实现非阻塞轮询,这里我们使用while循环来实现!

信号是没有0号信号的!这是因为0号代表进程正常执行,没有异常,即没有信号干扰! 

waitpid 中的 options 参数用于控制该函数的行为模式,允许父进程以更灵活的方式等待子进程的状态变化。以下是 options 参数的详细解析:

主要选项及其作用

  1. 0(默认阻塞模式)

    • 当 options 设置为 0 时,waitpid 会以阻塞方式等待子进程结束或状态变化。父进程会暂停执行,直到满足等待条件(如子进程终止)后才继续运行。
    • 适用场景:需要父进程必须等待子进程结束后才能继续执行的简单场景 。
       
  2. WNOHANG(非阻塞模式)

    • 若子进程尚未结束或状态未变化,waitpid 会立即返回,而非阻塞父进程。此时返回值可能是:
      • 子进程 PID:子进程已结束,状态被成功收集。
      • 0:子进程仍在运行,但未结束。
    • 适用场景:父进程需要同时处理其他任务(如轮询多个子进程状态)时,避免阻塞以提高效率 。
       
  3. WUNTRACED(报告停止状态)

    • 当子进程因信号(如 SIGSTOP被暂停(而非终止)时,waitpid 会返回其状态。
    • 需配合宏 WIFSTOPPED(status) 和 WSTOPSIG(status) 解析状态,获取导致暂停的信号(如 SIGSTOP) 。
  4. WCONTINUED(报告继续状态)

    • 当子进程因 SIGCONT 信号恢复执行时,waitpid 会返回其状态。需通过宏 WIFCONTINUED(status) 检测此状态 。
       

三、进程替换

excl类函数的作用:执行一个文件;

标准写法要以NULL为结尾! (execl后面的代码不会执行!)

单进程的程序进行程序替换:

        当我们名为mycommand的程序进行运行的时候,刚开始printf进行打印,执行到execl这一行命令的时候,此时会将ls的代码和数据移至内存当中(对原来的数据和代码进行覆盖)!此时整个页表左侧的结构和东西不变!(task_struct和进程地址空间和页表左侧) 

当我们运行到程序替换所对应的程序的位置的时候:将新程序的数据替换老程序的数据;

页表对应的左侧都没有发生变化!此时是页表对应的右侧的物理内存发生了变化,将新的程序加载到内存当中并替换页表;

execl如果执行成功!此时后面的代码都不会被执行!哪怕此时后面存在exit(0);

但是如果execl执行失败,此时函数会返回1,然后继续执行后面的代码!(execl后面的代码和数据也算是老程序的代码和数据)

问题:子进程执行程序替换的时候,是否会影响父进程?

当子进程进行进程替换的时候,不会影响父进程!此时子进程会进行写时拷贝!

程序替换不会创建子进程!只进行程序的代码和数据的替换工作!

程序替换的现象:

  • 程序替换之后,exec* 之后的代码不会执行,替换失败呢,才有可能执行后面的代码;
  • exec* 函数只有失败才有返回值!!成功没有!

问题:当我们加载新的程序的时候,CPU如何知道新的程序的入口地址?

  • Linux中的可执行程序是有格式的!(ELF),在可执行程序的表头中有可执行程序的入口地址;
  • 新进程的表头可以被CPU读取,进行替换新的进程;

execl接口介绍

list指的是像链表一样一个节点一个节点的进行传递(参数为可变参数); 

所有的exec* 系列的函数的第一个参数都是为了找到执行该程序的地址;

找到该程序后应该怎么办?

主要是确定如何执行该程序,该程序需不需要覆盖选项?(命令行中如何传,此时我们就如何传)

execlp接口介绍

带p指的是:PATH,也就是说execlp会在自己默认的PATH中进行查找!

虽然这里显示我们调用写了两个ls,但是实际上!第一个参数ls是为了确定在哪里找到整个程序(即我们需要执行谁)!第二个ls是为了确定我们需要怎么进行执行!

execv接口介绍

这里的v指的是数组!

以NULL作为结尾!

实际上就是将我们对应的可变参数列表替换为了字符串指针数组!

这里在使用的时候我们需要先定义一个数组!

char* const myargv[] 

ls有main函数,那么ls的main函数有命令行参数吗?

有!这里ls的命令行参数是由myargv传入的!

execvp接口介绍

实际上就是vector + PATH!

此时调用更加简单! 

execle接口介绍

这里的e指的是env:也就是我们自己维护的环境变量!

补充点:对于C++的程序,后缀吹了.cpp,还可以为.cc或.cxx;

如何通过一个Makefile形成两个或者多个可执行文件?

可以定义一个伪目标文件all;

此时形成这个伪目标文件需要先形成其他两个可执行程序! 

当前我们有两个可执行程序!此时我们想要通过mycommand来调用otherExe!

我们可以通过下面的格式进行调用!

  • 第一个参数表示的是执行程序的路径(相对路径);
  • 第二个参数是我们需要执行什么程序;
  • 第三个参数是附加的选项(这里为NULL!);

问题:C/C++语言程序是否可以调用其他语言或脚本的程序?

答案:可以!

脚本语言开头都以#!为开头!后面对应的是脚本语言的解释器!

假如说我们当前有如下所示的脚本文件:

./shell脚本不能执行!(得通过bash来执行!)

bash  test.sh 

此时如果我们想通过C/C++语言程序执行这个脚本,那我们应该怎么执行?

这里得第一个参数为bash解释器得地址!

第二个为需要执行的指令!(按照在命令行输入指令的形式)

如果当前我们有一个Python脚本也是对应的!

问题:无论是我们的可执行程序,还是脚本语言,为什么都能跨语言调用呢?

这是因为所有语言运行出来,本质上都是进程! 

问题:我们的exec* 能调用系统指令,那么能不能执行我们自己的指令?

一个程序代码是否能通过exec*传递命令行参数,另一个可执行程序通过main中的命令行参数接受到可执行参数?

这里我们没有传递环境变量,但是子进程依然可以显示环境变量!

问题:环境变量是什么时候传递给子进程的? 

结论:环境变量也是数据,创建子进程的时候,环境变量就已经被子进程继承下去了!(extern cahr** environ)

所以程序替换中,环境变量信息不会被替换!

问题:为什么程序被替换的时候,环境变量信息不回被替换?(deepseek)

1. 环境变量的存储位置与程序替换的覆盖范围

  • 环境变量存储区域: 环境变量通常存储在进程地址空间的 栈区之上独立的环境变量表 中,与程序的代码段(.text)、数据段(.data)分离。程序替换仅覆盖代码段和数据段,而不会修改栈区和堆区等内存区域,因此环境变量所在区域未被触及。

  • 程序替换的本质: 替换过程通过加载新程序的代码和数据到内存中,并更新页表映射,但不会修改进程的上下文信息(如环境变量、文件描述符等)。

2. 环境变量的继承机制

  • 父子进程的继承性: 子进程通过 fork 创建时,会复制父进程的环境变量表。即使后续调用 exec 替换程序,子进程仍保留父进程传递的环境变量,除非显式指定新的环境变量数组。

  • 默认行为exec 系列函数(如 execlexecv 等)默认使用父进程的环境变量,仅当调用 execleexecve 时才会通过参数 envp[] 覆盖式传递 新环境变量。例如:

char *envp[] = {"PATH=/custom/path", NULL};
execle("/bin/program", "program", NULL, envp);  // 覆盖原有环境变量

3. 操作系统对进程上下文的管理

  • 进程控制块(PCB)的独立性: 程序替换仅修改进程的代码和数据,而 PCB 中的优先级、环境变量指针等元数据保持不变。环境变量作为进程运行时的上下文信息,独立于代码逻辑。

  • 性能优化: 环境变量的继承避免了重复加载和初始化,提升进程创建效率。若每次替换程序都重新加载环境变量,会增加系统开销。

问题:如果我们想给子进程传递环境变量,此时应该怎么进行传递呢?

  • 新增环境变量;

第一种情况:我们可以在bash上面新增一个环境变量,此时 mycommand是bash的子进程,会继承该环境变量;otherExe是mycommand的子进程,会继承其的环境变量,最后得到新增的环境变量!

第二种情况:我们不想让bash有该环境变量!

需要在父进程中导入!

我们可以使用上面的系统调用接口新增环境变量! 

第三种情况:通过exec* 系列带e的函数可以实现:

但是上面实际还是将父进程的环境变量传递过去,默认的也是将父进程的传过去! 

  • 彻底替换;

我们可以通过自定义环境变量,然后进行替换! 

结论:这里execle采用的策略是覆盖而不是替换!

man的3号手册一般被称为库函数!(2号手册是系统调用!)

这里的execve实际上是系统调用接口!

因此实际上其他库函数最后都是调用execve这个系统调用函数!

再谈shell 

shell被称为外壳程序,shell/bash也被称为一个进程,执行命令的时候,本质上是创建子进程!

当我们在bash上面执行命令的时候,此时左侧对应的用户名 + 主机名 + 路径等,我们可以通过环境变量来获取(系统调用也可以!)

自定义简单shell

补充点1:C语言中,相邻字符串具有自动连接的特点!

如下所示:

printf("This is a very long string ""that spans multiple lines ""in the source code.");

输出结果:This is a very long string that spans multiple lines in the source code.

补充点二:fgets函数的用法(deepseek)

        在Linux系统中,fgets()是C标准库中用于安全读取字符串的函数,尤其适合处理文本文件的逐行读取需求。以下是其核心特性、使用方法和注意事项的综合解析:

  • 参数解析
    • str:指向字符数组的指针,用于存储读取的字符串。
    • n:最大读取字符数(包括结尾的\0),即最多读取n-1个字符。
    • stream:输入流指针,可以是文件指针(如fopen()返回的)或标准输入(stdin) 。

返回值

  • 成功时返回str指针;
  • 失败或到达文件末尾时返回NULL,需通过feof()ferror()判断具体原因 。

assert在编译的时候起效果,运行的时候没有效果!

注意点一:

对有的编译器来说,有时候创建变量若没有使用,则此时会出现警告甚至是报错,因此此时我们可以采用下面这种方式:

通过(void)s 避免报错;

注意点二:

这里我们为了不想当输入完指令后,此时回车也会被记录到对应的指令中,因此此时我们可以进行下面的处理:

这里的strlen会带上\n,因此此时我们将对应的\n替换为\0,即可实现!

注意点三:

为实现不同的功能,这里我们需要对字符串进行分割,这里我们调用一个函数strtok;

 strtok是C标准库中用于分割字符串的核心函数,常用于解析以特定分隔符分隔的文本数据。

#include <string.h>
char *strtok(char *str, const char *delim);
  • 参数
    • str:首次调用时传入待分割的字符串;后续调用需设为NULL,以继续处理剩余部分。
    • delim:分隔符集合字符串,函数会将其中任意字符视为分隔符。
  • 返回值:返回指向当前子串的指针,若无可分割内容则返回NULL 。
     

该函数一次只会分割一次字串!

strtok的基本用法:

#include <stdio.h>
#include <string.h>int main() {char str[] = "apple,banana;cherry";char *token = strtok(str, ",;"); // 分隔符可以是逗号或分号while (token != NULL) {printf("%s\n", token);token = strtok(NULL, ",;"); // 后续调用传入NULL}return 0;
}

这里需要注意的是,后续用来分隔的时候,需要传入NULL;

输出如下所示:

apple
banana
cherry

当我们执行mkdir等的时候,都可以正常运行,但是执行cd ..  或者 cd /等却不能正常运行!

这是因为这些命令我们都是通过fork之后的子进程来运行的!但是子进程的运行结果不会影响父进程!

因此这些命令需要父进程自己来运行!这些命令也就成为内建命令!

在自定义的shell时,内建命令需要我们自己罗列起来;

引用新的系统调用接口:chdir

在Linux系统中,chdir函数是用于更改进程当前工作目录的核心接口,属于C标准库(libc)的一部分。

函数原型:

#include <unistd.h>
int chdir(const char *path);
  • 参数path可以是绝对路径(如/home/user)或相对路径(如../doc) 。
     
  • 返回值:成功返回0,失败返回-1并设置errno以指示具体错误 。

函数的使用场景:

#include <stdio.h>
#include <unistd.h>int main() {char buf[256];getcwd(buf, sizeof(buf));  // 获取当前目录printf("原目录: %s\n", buf);if (chdir("/tmp") == 0) {  // 切换目录到/tmpgetcwd(buf, sizeof(buf));printf("新目录: %s\n", buf);} else {perror("chdir失败");}return 0;
}

输出结果如下所示:

原目录: /home/user
新目录: /tmp

引用函数调用接口:sprintf

   sprintf 是 C 语言标准库中用于将格式化数据写入字符数组的核心函数,其作用是将变量、常量等数据按指定格式组合成一个字符串,并将结果存储到用户提供的缓冲区中。

函数原型与参数

int sprintf(char *str, const char *format, ...);
  • str:指向目标字符数组的指针,用于存储结果字符串。需确保缓冲区足够大,否则可能引发溢出 。
     
  • format:格式字符串,包含普通字符和格式说明符(如 %d%s)。格式说明符决定参数的类型和显示方式 。
     
  • ...:可变参数列表,参数数量与格式字符串中的 % 标签数量一致 。

 示例场景:

int num = 123;
char buffer[20];
sprintf(buffer, "%d", num);  // 转换为 "123"

引用函数调用接口:getcwd

getcwd 是用于获取进程当前工作目录(Current Working Directory)的 C 标准库函数,其底层通过系统调用实现。

#include <unistd.h>
char *getcwd(char *buf, size_t size);
  • 参数
    • buf:存储路径的缓冲区,若为 NULL 且 size=0,函数自动分配内存(需手动 free 释放)。
    • size:缓冲区大小,若路径长度超过 size,返回 NULL 并设置 errno=ERANGE 。
  • 返回值:成功返回路径指针,失败返回 NULL 。

这里我们还需要处理export和echo打印环境变量!

这里export也需要是内建命令!如果不是内建命令,当我们到入环境变量时,通过fork子进程导入对应的环境变量,此时父进程无法显示!但是如果父进程执行该命令,此时由于继承环境变量,子进程也会有新的环境变量!

当我们们登录的时候,系统会帮我们启动一个shell进程,此时可以引出问题:shell本身的环境变量表是从哪里得来的?

命令行中所有执行的环境变量都是从bash得到的。

在当前用户的家目录下,有这个.bash_profile这个文件!

即当用户登陆的时候,shell会读取用户目录下的.bash_profile文件,里面保存了导入环境变量的方式!


http://www.ppmy.cn/ops/165457.html

相关文章

[从零开始学习JAVA] 新版本idea的数据库图形化界面

前言: 在看黑马javaweb的时候&#xff0c;发现视频中的版本是老版本,而我的是新版本 为了记录新版本的数据库界面图形化操作我打算写下这篇博客 案例 创建tb_user表 对应的结构如下 要求 1.id 是一行数据的唯一标识 2.username 用户名字段是非空且唯一的 3.name 姓名字…

Javascript ajax

9.1 学习ajax的前置知识——JSON JSON是什么 JSON(JavaScript Object Notation)是⼀种轻量级的数据交换格式&#xff0c;它基于JavaScript的⼀个⼦集&#xff0c;易于⼈的编写和阅读&#xff0c;也易于机器解析。 JSON采⽤完全独⽴于语⾔的⽂本格式&#xff0c;但是也使⽤了类似…

宇树人形机器人开源模型

1. 下载源码 https://github.com/unitreerobotics/unitree_ros.git2. 启动Gazebo roslaunch h1_description gazebo.launch3. 仿真效果 H1 GO2 B2 Laikago Z1 4. VMware: vmw_ioctl_command error Invalid argument 这个错误通常出现在虚拟机环境中运行需要OpenGL支持的应用…

iOS UICollectionViewCell 点击事件自动化埋点

iOS 中经常要进行埋点&#xff0c;我们这里支持 UICollectionViewCell. 进行自动化埋点&#xff0c;思路&#xff1a; 通过hook UICollectionViewCell 的setSelected:方法&#xff0c; 则新的方法中执行埋点逻辑&#xff0c;并调用原来的方法 直接上代码 implementation UICol…

Rust规律归纳随笔

1. 针对所有权规则&#xff1a; 唯一所有权 <------------> 智能指针(引用计数)<-------------->共享所有权 (引用计数) { 单线程&#xff1a;Rc 多线程&#xff1a;Arc } 2. 针对借用规则&#xff1a; 共享不可变&#xff08;多读&#xff09;<----->…

聊聊langchain4j的AiServicesAutoConfig

序 本文主要研究一下langchain4j-spring-boot-starter的AiServicesAutoConfig LangChain4jAutoConfig dev/langchain4j/spring/LangChain4jAutoConfig.java AutoConfiguration Import({AiServicesAutoConfig.class,RagAutoConfig.class,AiServiceScannerProcessor.class })…

【性能测试】Jmeter如何做一份测试报告(3)

本篇文章主要介绍Jmeter中下载插件&#xff08;Jmeter Plugins&#xff09; 如何使用监听器插件&#xff0c;线程组插件&#xff0c;梯度压测线程组 测试报告需要去关注的数据&#xff0c;怎么看测试报告图表 目录 一&#xff1a;插件下载 1&#xff1a;下载地址 2&#xff…

[mybatis]resultMap详解

resultMap Mybatis中提供了resultMap功能&#xff0c;可以将数据库查询结果映射到Java对象&#xff0c;用于解决 字段名与属性名不一致 或 复杂关系&#xff08;如一对多&#xff09;的映射问题。 比如一个User类&#xff0c;在它的属性里还有另一个子对象&#xff08;或者多…