【Linux】进程控制

🌻个人主页路飞雪吖~

🌠专栏:Linux


目录

一、进程创建

🌟fork函数初识

🌟fork函数返回值

🌟写时拷贝

🌟fork常规用法

🌟fork调用失败的原因

二、进程终止

🌟进程退出场景

🌟进程常见的退出方法

✨_exit函数(偏系统)

✨exit函数

✨return退出

[🌠exit vs _exit](#🌠exit vs _exit)

三、进程等待

🌟等待进程的必要性

🌟进程等待的方法

✨wait方法

✨waitpid方法

✨获取子进程status

四、进程程序替换

🌟替换函数

🌟替换原理

🌟函数解释

[✨多进程方式 【命令行解释器原理】](#✨多进程方式 【命令行解释器原理】)

✨多进程原理

✨命令行参数和环境变量

🌟命名理解

​编辑

🌠关于环境变量:


一、进程创建

🌟fork函数初识

在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

cpp 复制代码
#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1

🌠进程调用fork,当控制转移到内核中的fork代码后,内核做:

• 分配新的内存块和内核数据结构给子进程;

• 将父进程部分数据结构内容拷贝至子进程;

• 添加子进程到系统进程列表当中;

• fork返回,开始调度器调度 。

🌟fork函数返回值

• 子进程返回0

• 父进程返回的是子进程的pid。

🌟写时拷贝

通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副 本。具体见下图:

当父进程创建子进程时,父进程直接将 父进程页表的执行权限,全部改成只读,创建子进程时继承下来的子进程页表就是全部都是只读的,所以对于代码段本来就是只读的,当用户对代码段进行写入时,就会出错; 数据有可能要被修改的,当页表识别到数据段在进行写入时,就会触发系统错误,触发 缺页中断 ,系统就会去进行检测。判定:检测到是数据段进行写入,就会杀掉这个进程,检测到是数据段写入时,就判断要进行写时拷贝--> 申请内存 --> 发生拷贝 --> 修改页表 --> 恢复执行,最后把数据段的权限区域恢复成读写权限。

你的写入操作 != 对应目标区域进行覆盖时才做,如:count++;

为什么要拷贝,而不是直接申请空间?当你修改时,并不是对目标区域进行100%的覆盖,可能只是基于历史数据进行修改(count++)。

🌟fork常规用法

• 一个父进程希望复制自己,使父子进程同时执行不同的代码段。(创建子进程目的是为了让子进程执行父进程的一部分代码)例如,父进程等待客户端请求,生成子 进程来处理请求。

• 一个进程要执行一个不同的程序。(子进程被创建出来是为了执行新的程序的)例如子进程从fork返回后,调用exec函数。

🌟fork调用失败的原因

• 系统中有太多的进程

• 实际用户的进程数超过了限制

二、进程终止

🌟进程退出场景

main函数的返回值:返回给父进程 或者 返回给系统。

表明错误原因:

0:成功,非0:错误,用不同的数字,约定或者表明出错的原因。系统提供了一批错误码。

1、进程退出时,main函数结束,代表进程退出;main函数的返回值,表示进程的退出码;

2、进程退出码,可以由系统默认的错误码来提供,也可以自定义去约定

🌟进程常见的退出方法

✨_exit函数(偏系统)

作用:终止调用进程。

cpp 复制代码
#include <unistd.h>

void exit ( int status); 

参数:status 定义了进程的终止状态,父进程通过wait来获取该值;

✨exit函数

• exit:在代码的任何地方直接退出,表示当前进程结束。

✨return退出

表示函数退出。函数退出后,继续执行下一个函数。执行return等同于执行 exit(n),因为调用main的运行时,函数会将main的返回值当做exit的参数。

🌠exit vs _exit

• 刷新缓冲区的问题

exit:会直接把缓冲区的数据输出;

_exit:不会输出缓冲区的数据;

缓冲区,一定不在操作系统内部!

缓冲区一定不属于操作系统提供的缓冲区,它跟系统没关系。如果中国缓冲区是在操作系统内部,那printf输出的这个消息也一定在操作系统当中,所以当进程退出的时候,不管是exit还是_exit都应该刷新缓冲区,但事实是只有_exit才会刷新缓冲区,这种叫做语言级缓冲区(C/C++),和操作系统没关系。

• exit (man 3 exit)属于语言级别;

• _exit(man 2 _exit)属于系统级别,属于系统调用;

三、进程等待

🌟等待进程的必要性

• 子进程退出,父进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存泄漏。
• 另外,进程一旦变成僵尸状态,kill -9 也杀不掉,因为谁也没有办法杀死一个已经死去的进程。
• 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
• 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

🌟进程等待的方法

对于父进程创建出来的子进程 ,创建出来就必须对子进程负责,作为父进程必须得等待子进程。需要知道子进程完成的怎么样。

✨wait方法

复制代码
#include<sys/types.h>
#include<sys/wait.h>
 
pid_t wait(int*status);
 
返回值:
 成功返回被等待进程pid,失败返回-1。
参数:
 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
cpp 复制代码
man 2 wait

等待一个进程,直到这个进程更改它的状态。

<1> pid_t wait ( int *status ) :

一般而言,父进程创建子进程,父进程就要等待子进程,直到子进程结束;

(当子进程完成任务后,通过wait,让父进程回收子进程的僵尸状态,当父进程调用wait期间,父进程要等待子进程,等待的时候,子进程不退出,父进程就要阻塞在wait函数内部![类似于scanf,当键盘不输入就会卡在那里])

<2> 返回值 pid_t :

pid_t > 0 成功回收了一个子进程;

pid_t < 0 回收子进程失败;

1、父进程要回收子进程的僵尸状态;

2、等待任意一个子进程。

• wait在进行等待子进程期间,会阻塞式等待,等待成功的返回值,一般为等待子进程的pid。

1、创建子进程就必须得回收子进程的状态;

2、创建子进程,作为父进程要知道子进程运行的怎么样?要获得子进程的退出信息!【退出码】。

✨waitpid方法

cpp 复制代码
man waitpid

🌻返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

🌻参数: pid
• pid > 0:等待成功,目标为子进程的pid;

• pid == 0:等待成功,但是子进程没有退出;

• pid = -1:等待失败,等待任一个子进程。与wait等效。

🌻status:
WIFEXITED(status) : 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status) : 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

🌻options:
• WNOHANG: 【非阻塞等待】若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。非阻塞轮询,由自己循环调用非阻塞接口,完成轮询检测。可以让自己做更多自己的事情。

• 若正常结束,则返回该子进程的pid。

• 0 :阻塞等待。

• 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
• 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
• 如果不存在该子进程,则立即出错返回。

waitpid可以回收子进程的僵尸状态,但是父进程被一直阻塞在这里,

使得父进程后面的内容都没有跑,阻塞在这里等待子进程退出,并不知道子进程什么时候退出。

✨获取子进程status

• int *status :帮助父进程获取子进程的退出信息。(输出型参数)

子进程的退出信息,会保存在子进程的PCB里面,当waitpid获取子进程的退出信息的时候,操作系统就会从子进程的PCB里面把错误信息写到status里面。status是一个输出型参数,调用waitpid完成之后,从操作系统内部把子进程的退出信息带出来【不是通过返回值带出来的】。

status不仅仅包含进程退出码【正常退出的信息】! 进程退出【return、exit、_exit都为进程正常结束】。

status不是一个完整的整数,它是采用 位图 结果来设置的,一共32个Bite,我们只考虑低16位,它的次第8位才为退出码。

我们能不能使用全局变量,来获取子进程的退出码呢【exit(123);】?不可以,父子进程的数据是各自私有一份的,当子进程修改了父进程是看不到子进程的数据的【父子进程的数据具有独立性】,地址一样,内容不同, 因此我们只能通过系统调用获取子进程对应的退出信息。

🌠进程退出:

<1> 按照退出码判定:

1、代码跑完,结果对,return 0;

2、代码跑完,结果不对, return !0;

<2> 进程退出信息中,会记录下来自己的退出信息:

3、进程异常

• 程序员自身编码错误导致出错,OS提前使用信号终止了你的进程。

我们在拿status获取退出信息时,我们即会获得退出码,又会获得进程退出信号。

一个进程它的结果是否正确,前提条件必须是它的退出信号的值为0,即没有收到退出信号,说明它的代码是正常跑完的,但是结果的正确与否,要通过退出码来判断。

一个进程直接崩溃,是因为先有错误,然后被计算机发现错误,如:虚拟地址到物理地址转换出错【野指针,异常访问】,当操作系统发现错误,一般都是通过信号来杀掉这个进程的。

如果父进程所创建的子进程,当子进程在执行对应的代码的时候,代码执行的结果正确与否,父进程一清二楚,代码跑没跑完由退出码决定,中间有没有异常由信号决定,既没有异常,退出代码又没有异常,代码肯定是跑成功了。

🌠小贴士:

<1> 让子进程帮我完成某种任务:

cpp 复制代码
  1 #include <iostream>
  2 #include <vector>
  3 #include <cstdio>
  4 #include <unistd.h>
  5 #include <sys/types.h>
  6 #include <sys/wait.h>
  7 
  8 enum 
  9 {
 10     OK = 0,
 11     OPEN_FILE_ERROR = 1,
 12 };
 13 
 14 const std::string gsep = " ";
 15 std::vector<int> data;
 16 
 17 int SaveBegin()
 18 {
 19     std::string name = std::to_string(time(nullptr));
 20     name += ".backup";
 21     FILE *fp = fopen(name.c_str(),"w");
 22     if(fp == nullptr) return OPEN_FILE_ERROR;
 23 
 24     std::string dataStr;
 25     for(auto d : data)
 26     {
 27         dataStr += std::to_string(d);
 28         dataStr += gsep;
 29     }
 30 
 31     fputs(dataStr.c_str(), fp);
 32     fclose(fp);
 33     return OK;
 34 }
 35 
 36 void save()
 37 {
 38     pid_t id = fork();
 39     if(id == 0)// 子进程完成特定的任务
 40     {
 41         int code = SaveBegin();
 42         exit(code);                                                                                        
 43     }
 44     // 父进程可以获得子进程的退出信息
 45     int status = 0;
 46     pid_t rid = waitpid(id, &status,0);
 47     if(rid > 0)
 48     {
 49         int code = WEXITSTATUS(status);// 获得子进程的退出码
 50         if(code == 0) printf("备份成功, exit code: %d\n",code);
 51         else printf("备份失败, exit code: %d\n",code);
 52     }
 53     else 
 54     {
 55         perror("waitpid");
 56     }
 57 }
 58 
 59 int main()
 60 {
 61     int cnt = 1;
 62     while(true)
 63     {
 64         data.push_back(cnt++);
 65         sleep(1);
 66 
 67         if(cnt % 10 == 0)
 68         {
 69             save();
 70         }
 71     }
 72     // 每隔10s,备份一次数据
 73     
 74 }

<2>阻塞与非阻塞的问题:

cpp 复制代码
  1 #include <iostream>
  2 #include <vector>
  3 #include <cstdio>
  4 #include <unistd.h>
  5 #include <sys/types.h>
  6 #include <sys/wait.h>
  7 #include <functional>
  8 #include "task.h"
  9 
 10 typedef std::function<void()> task_t;
 11 
 12 void LoadTask(std::vector<task_t> &tasks)
 13 {
 14     tasks.push_back(PrintLog);
 15     tasks.push_back(Download);
 16     tasks.push_back(Backup);
 17 }
 18 
 19 int main()
 20 {
 21     std::vector<task_t> tasks;
 22     LoadTask(tasks);
 23 
 24     pid_t id = fork();
 25     if(id == 0)
 26     {
 27         // child
 28         while(true)
 29         {
 30             printf("我是子进程,pid:%d\n",getpid());
 31             sleep(1);
 32         }
 33         exit(0);
 34     }
 35     //father
 36     while(true)
 37     {
 38         sleep(1);
 39         pid_t rid = waitpid(id, nullptr, WNOHANG);// 非阻塞等待
 40         if(rid > 0)
 41         {
 42             printf("等待子进程%d成功\n",rid);                                                                                         
 43             break;
 44         }
 45         else if(rid < 0)
 46         {
 47             printf("等待子进程%d失败\n",rid);
 48             break;
 49         }
 50         else
 51         {
 52             printf("子进程尚未退出\n");
 53 
 54             //父进程做自己的事情(非阻塞轮询)
 55            for(auto &task : tasks)
 56            {
 57                 task();
 58            }
 59         }
 60     }
 61 
 62     return 0;
 63 }

四、进程程序替换

🌟替换函数

cpp 复制代码
man execl
man execve

🌟替换原理

当我们运行程序(./myexec进程)时,先创建PCB,接着有自己的虚拟地址空间,也要有自己对应的可执行程序,可执行程序要加载到物理内存,然后经过页表和虚拟地址空间进行映射。

当我们在代码调用 execl() 函数 ,磁盘里还存在另一个程序,当我们调用execl()函数的时候,其中这另一个程序,会把【./myexec】的代码段和数据段覆盖掉,即我们对应的当前进程【./myexec】自己的代码和数据直接用新目标程序的代码和数据覆盖掉,包括栈、堆给清空掉。

进程替换不是创建新进程 ,进程 = 内核数据结构 + 代码和数据 ,替换只替换程序的代码和数据,更改页表的映射,进程的PCB信息根本不会变化。

侧面说明,execl()函数,不仅可以替换系统命令,也可以替换我们自己写的程序。

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动 例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

🌟函数解释

✨execl函数的返回值:

• 成功的时候,没有返回值。

• 失败返回 -1 。

返回成功的时候,就已经把代码和数据给覆盖掉了【int n 、printf都已经被覆盖掉了】,所以成功时候没有返回值;返回失败,就是没有覆盖成功。

即execl()函数只要返回,就是失败!

• 如何理解一个可执行程序被加载到内存的?

一个进程把它的代码和数据放到到内存里,就是要给进程开辟空间或者重新去进行覆盖,即为加载。

✨多进程方式 【命令行解释器原理】

父进程创建子进程,让子进程去加载:这样子父进程就不会被覆盖了,加载出错只影响子进程。

fork()一下,由子进程执行对应的程序,子进程退出的时候并不影响父进程。 我们要执行什么命令都是固定写死的【ls -l -a】,我们可以在子进程里面灵活的执行任何命令。

用户执行任何任务,最终都是以进程来实现的,操作系统会执行我们对应的任务,所有语言写出来的程序都会直接/间接转化为进程。

✨多进程原理

当要替换的时候,先创建子进程,创建的子进程也会指向父进程对应的堆、栈、数据段、代码段,当子进程要替换新程序时,替换代码段和数据段,此时就会发生写时拷贝。【覆盖就是把数据进行修改,数据要修改就会发生写时拷贝】,操作系统就会把子进程所要访问的任何区域,全部都要发生写时拷贝一份,所以子进程和父进程就独立开了。 进程的数据时独立的,代码时共享的,数据可以发生写时拷贝,当程序替换时,代码就会被覆盖,就可以执行新的程序。此时进程就可以彻底独立!

🌠程序替换:

1、调用 execl() 函数 接口;

2、 必须从新程序的 main函数 开始运行!

✨命令行参数和环境变量

命令行参数,是怎么传递给所写程序的?谁传递的?

命令行构建对应的数组,通过execv(),把参数传递进去,execv()是系统,系统就可以找到 ls 的main函数,把shell命令行/自己构建的argv参数,传递给ls的main函数,包括元素个数,去遍历它,直到nullptr结束。

🌟命名理解

• l(list) : 表示参数采用列表
• v(vector) : 参数用数组
• p(path) : 有p自动搜索环境变量PATH
• e(env) : 表示自己维护环境变量

cpp 复制代码
        我要执行谁?(带路径)      我想怎么执行?
int execv(const char *path, char *const argv[]); 
// v(vector) : 参数用数组
cpp 复制代码
      你想运行谁?(不要求带路径)  你想怎么执行?
int execlp(const char *file, const char *arg, ...);
// p(path) : 有p自动搜索环境变量PATH

• 为什么可以不带路径呢?

我们在系统当中查找任何的可执行程序,都有一个环境变量【PATH,全局属性】,execl、execv要求具有带全路径,是因为它们不会主动地去PATH里面去找,环境变量是全局属性,任何进程都可以查,启动的进程会继承父进程的bash,在执行execlp会自动去PATH里面去找。

cpp 复制代码
 int execvp(const char *file, char *const argv[]);
cpp 复制代码
int execvpe(const char *file, char *const argv[],char *const envp[]);

🌠关于环境变量:

<1> 让子进程继承父进程全部的环境变量;

<2> 如果要传递全新的环境变量(自己定义,自己传递);

<3> 新增环境变量:

cpp 复制代码
man putenv  // 新增环境变量

程序替换不影响命令行参数和环境变量。

如若对你有帮助,记得关注、收藏、点赞哦!您的支持是我最大的动力🌹🌹🌹🌹!!!

若有误,望各位,在评论区留言或者私信我 指点迷津!!!谢谢^ ^ ~

相关推荐
易保山3 分钟前
MIT6.S081 - Lab9 File Systems(文件系统)
linux·操作系统·c
林开落L7 分钟前
Linux深度探索:进程管理与系统架构
linux·运维·系统架构
XINO13 分钟前
防火墙双机热备实践
运维·安全
神洛华24 分钟前
Docker概念详解
运维·docker·容器
四川合睿达自动化控制工程有限公司24 分钟前
管道位移自动化监测方案
运维·自动化
007php00727 分钟前
Docker Compose 安装Elasticsearch8和kibana和mysql8和redis5 并重置密码的经验与总结
大数据·运维·elasticsearch·搜索引擎·docker·容器·jenkins
城南已开97943 分钟前
vue部署到nginx服务器 启用gzip
服务器·vue.js·nginx
XINO1 小时前
企业常见安全事故排查思路
运维·安全
林政硕(Cohen0415)1 小时前
在ARM Linux应用层下驱动MFRC522
linux·mfrc522·ic-s50·m1卡
艾伦_耶格宇1 小时前
shell 脚本实验 -5 while循环
linux