【Linux】进程控制

发布于:2024-12-22 ⋅ 阅读:(11) ⋅ 点赞:(0)

🌻个人主页路飞雪吖~

       🌠专栏:Linux


目录

一、进程创建

🌟fork函数初识

🌟fork函数返回值

🌟写时拷贝

🌟fork常规用法

🌟fork调用失败的原因

二、进程终止

🌟进程退出场景

🌟进程常见的退出方法

✨_exit函数(偏系统)

✨exit函数

✨return退出

  🌠exit vs _exit 

三、进程等待

🌟等待进程的必要性

🌟进程等待的方法

✨wait方法

✨waitpid方法

✨获取子进程status

四、进程程序替换

🌟替换函数

🌟替换原理

🌟函数解释

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

✨多进程原理

✨命令行参数和环境变量

🌟命名理解

​编辑

🌠关于环境变量:


一、进程创建

🌟fork函数初识

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

#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函数(偏系统)

作用:终止调用进程。  

#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
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方法

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> 让子进程帮我完成某种任务:

  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>阻塞与非阻塞的问题:

  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 }

四、进程程序替换

🌟替换函数

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) : 表示自己维护环境变量 
 

        我要执行谁?(带路径)      我想怎么执行?
int execv(const char *path, char *const argv[]); 
// v(vector) : 参数用数组

      你想运行谁?(不要求带路径)  你想怎么执行?
int execlp(const char *file, const char *arg, ...);
// p(path) : 有p自动搜索环境变量PATH

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

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

 int execvp(const char *file, char *const argv[]);

int execvpe(const char *file, char *const argv[],char *const envp[]);

🌠关于环境变量:

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

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

<3> 新增环境变量:

man putenv  // 新增环境变量

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

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

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