🌠专栏:Linux
目录
[🌠exit vs _exit](#🌠exit vs _exit)
[✨多进程方式 【命令行解释器原理】](#✨多进程方式 【命令行解释器原理】)
一、进程创建
🌟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> 让子进程帮我完成某种任务:
cpp1 #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>阻塞与非阻塞的问题:
cpp1 #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> 新增环境变量:
cppman putenv // 新增环境变量
程序替换不影响命令行参数和环境变量。
如若对你有帮助,记得关注、收藏、点赞哦!您的支持是我最大的动力🌹🌹🌹🌹!!!
若有误,望各位,在评论区留言或者私信我 指点迷津!!!谢谢^ ^ ~