目录
一、进程创建
1.1、fork()函数
1.2、fork的返回值
1.3、写实拷贝(Copy-on-Write,COW)
1.4、fork常规用法
1.5、fork调用失败的原因
二、进程退出
三、进程等待
1、wait和waitpid
1.1、解决僵尸进程问题
1.2、status参数
程序正常运行情况:
程序异常情况:
1.3、option参数
四、进程程序替换
1、快速认识进程程序替换
2、程序替换的接口
execl及其引出的相关概念:
子进程替换之后,代码也发生写实拷贝
替换可以替换我们自己写的程序
程序替换不会创建新的进程
其他接口:
五、自定义shell的编写
1、打印命令行提示符
2、获取命令行字符串
3、命令行解析
4、执行
5、处理内建命令
6、初始化环境变量表
7、完整代码
一、进程创建
1.1、fork()函数
# include <unistd.h>pid_t fork ( void );返回值:子进程中返回 0 ,父进程返回⼦进程 id ,出错返回 -1
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
分配新的内核数据结构即pcb以及mm_struct、vm_area_struct
1 #include<stdio.h>2 #include<unistd.h>3 #include<stdlib.h>4 int main()5 {6 pid_t pid;7 printf("Before: pid is %d\n",getpid());8 if((pid=fork())==-1)9 {10 perror("fork()");11 exit(1);12 }13 printf("After: pid is %d ,fork() return %d\n",getpid(),pid); 14 sleep(1);15 return 0;16 }
fork()创建子进程,给父进程返回子进程pid,给子进程自己返回0;那么3493就是父进程的pid,子进程pid就是3494


1.2、fork的返回值
1.3、写实拷贝(Copy-on-Write,COW)

原理:写时复制是一种延迟拷贝技术,多个进程在最初可以共享同一块内存区域,只有当其中某个进程尝试对这块共享内存进行写入操作时,系统才会为该进程创建一个独立的内存副本,让其在副本上进行写入,而其他进程继续使用原来的内存,以此避免不必要的内存拷贝,提高系统性能。 实现方式:Linux 通过页表机制来实现写时复制。当创建新进程时,内核会为新进程创建虚拟地址空间,并将父进程的页表复制一份给子进程。此时,父子进程的页表项都指向相同的物理内存页面,并将这些页面标记为只读。当某个进程试图写入共享页面时,CPU 会产生一个写保护错误,内核捕捉到这个错误后,会为该进程分配新的物理内存页面,将原来共享页面的内容复制到新页面中,然后更新页表项,使其指向新的页面,同时将新页面设置为可写,而其他进程的页表项仍指向原来的共享页面,不受影响。
优点 节省内存:在进程创建初期,多个进程共享相同的内存页面,无需为每个进程分配独立的内存空间来复制数据,只有在进程实际需要写入时才会分配新的内存,大大节省了内存资源,尤其在有大量进程同时运行且需要共享大量数据的情况下,能有效降低系统的内存压力。 提高进程创建速度:由于不需要在进程创建时立即进行大量的数据拷贝,只是复制页表等少量信息,使得进程创建的速度大幅提高。这对于需要频繁创建进程的应用场景,如服务器端的多进程并发处理等,能够显著提升系统的响应能力和整体性能。 减少数据不一致性:写时复制保证了在多个进程共享数据期间,只要没有进程进行写入操作,它们所访问的数据始终是一致的。只有当某个进程进行写入时,才会创建独立的副本,避免了对共享数据的意外修改,从而减少了数据不一致性的风险,提高了系统的稳定性和可靠性。 优化系统资源利用:写时复制机制使得系统能够更合理地分配内存资源,将有限的内存资源优先分配给真正需要写入数据的进程,避免了内存的浪费。同时,由于减少了内存拷贝操作,也降低了 CPU 的负担,使得系统资源能够得到更充分、有效的利用,提高了整个系统的运行效率。
1.4、fork常规用法
1.5、fork调用失败的原因
二、进程退出
程序运行最终的三种情况:
- 程序运行完毕,结果正确
- 程序运行完毕,结果不正确
- 程序异常终止
而最终收到一个程序最终运行结果的是其父进程;
从main函数入手:
main函数结束,return返回的是一个退出码,为0代表正常结束结果正确,非0表示结果不正确,并且这些非0的退出码都表示一个错误信息;程序异常终止也会返回一个退出码,但是这个退出码一定不是我们想要收到的退出码,也就是说程序异常终止时,返回一个无意义的退出码
其他函数的返回值只是一个单纯的值,不是退出码,只有main函数的return才是退出码
1 #include<stdio.h>2 3 int main()4 {5 FILE * fp = fopen("hellp.exe","r"); 6 if(fp == NULL)7 {8 return 1;9 }10 fclose(fp);11 return 0;12 }
echo &?打印最近一个进程的退出码,proc读文件失败返回1那么退出码为1,之后再打印,是打印的echo的退出码,为0
退出码写在这个进程的task_struct内部
退出码是给操作系统看的,想要查看每一个退出码的信息,用strerror(头文件<string.h>,总共134个退出码)
可以直接返回errno(头文件errno.h),就是这个退出码;也可以将errno放在strerror里面打印出错误码的错误信息
调用exit(头文件stdlib.h)也可以退出进程,任何地方调用exit()都是结束进程,返回给父进程退出码
exit(status),这个status就是错误码
调用_exit也能终止进程并且返回错误码
这两个有区别:exit是c语言库中的函数,_exit是系统调用接口层面的调用,库函数是系统调用的包装,也就是说exit调用的是_exit,但是exit退出时会刷新缓冲区,_exit不会刷新
看到这个例子:
首先是带\n表示,\n能够刷新缓冲区
此时exit、_exit所在代码的结果都一样;但是若是去掉\n,不让\n刷新缓冲区
exit还是会刷新缓冲器,最后打印main!
来看_exit:
此时没有打印main!,说明缓冲区没有被刷新,程序直接结束了
这就是两者的区别

三、进程等待
进程等待必要性:
1、子进程退出,父进程没有收到退出信息,子进程变为僵尸进程,这样的进程kill -9也无法杀死,因为不能杀掉一个已经死去的进程,那么父进程没有等待就会出现资源泄漏
2、父进程通过等待获取子进程任务完成如何
3、父进程通过等待获取子进程的退出信息从而来回收子进程的资源(这一点最重要)
1、wait和waitpid
1.1、解决僵尸进程问题
先来查看一个僵尸子进程,并且如何通过等待的方式来解决僵尸进程
子进程运行之后退出但是父进程不等待回收
子进程退出之后,成为僵尸进程Z
父进程等待wait解决僵尸进程
wait等待任意一个子进程结束,类型为pid_t,收到的是僵尸进程的pid;
status获取子进程退出信息,若是不关系则可以设置成NULL。
还有一点:当子进程没有退出时,因为fork分流,此时父进程也在执行,此时wait时没有子进程退出,那么父进程就处于阻塞状态,等待子进程的退出
waitpid:第二个参数和wait一样,第一个参数指定要等待的子进程pid,若是指定为-1,那么就是等待任意一个子进程pid,此时和wait一样
若是指定的pid不存在,那么等待失败,此时返回no child process错误码,表示没有这个子进程;并且此时waitpid返回值为-1
若是等待的pid不存在:
给id加1,则此id一定不存在
1.2、status参数
这是一个输出型参数,由操作系统填充
这个参数表示的是子进程的退出信息
程序正常运行情况:
子进程exit(1)引入话题:
子进程的退出码设置为1,status是子进程的退出信息,但是父进程打印的时候却不是1
那么status是怎么获得退出信息的?
status是一个整形遍历,32个比特位,高16为为0,次低8位表示正常运行时的退出信息
那么当exitcode为1的时候,status就是2^8
验证是不是这样,让父进程打印时打印(status>>8)&0XFF:
将子进程退出码改为10,再来验证
bash中echo $?获取退出码也是这样实现的:
那么定义一个exitcode全局变量,子进程退出时返回这个变量可以达到和status相同的效果吗?不行,这是因为这个全局变量在子进程中修改时会发生写实拷贝,由于进程的独立性,父进程是拿不到这个全局变量exitcode的;只能通过操作系统的系统调用接口拿到,就比如这里的waitpid();
程序异常情况:
那么当程序不是正常运行时,也就是进程退出的第三种情况,出现异常时status是怎么获取的?
当程序没有异常时,status的最低的7个比特位为0;一旦程序出现异常,最低的7个比特位表示的就是异常的退出信号;并且此时的退出码无意义。
异常信号有哪些?kill -l查看
验证异常:
子进程一直执行,在子进程运行时kill -9杀死进程,出现异常,父进程通过status收到退出信号,一定是SIGKILL 9 号信号:
继续验证:在子进程中设置异常代码:
等待具体是怎么做到的?
子进程的PCB中会存放exit_code、exit_signal退出信息
父进程通过操作系统调用系统接口wait或者waitpid获取这些退出信息;
getpid、getppid也是相似的原理;
理解为什么要有僵尸进程:
进程信息保留:子进程结束时,它需要向父进程返回一些信息,如退出状态、资源使用情况等。僵尸进程状态可以暂时保留这些信息,以便父进程通过特定的系统调用(如wait系列函数)来获取。如果没有僵尸进程状态,子进程结束后立即彻底消失,父进程就可能无法获取到这些重要信息,从而难以对其子进程的执行结果进行正确的处理和后续的资源回收。 父进程同步:僵尸进程的存在可以让父进程有机会与子进程的结束进行同步。父进程通过等待僵尸子进程的结束,可以确保在子进程完成所有任务并返回相关信息后,再继续执行后续的操作。这种同步机制有助于保证程序的正确性和稳定性,避免父进程在子进程还未完成时就进行一些依赖子进程结果的操作,从而导致错误或不一致的情况。 资源管理信号:僵尸进程对于操作系统来说,也是一种信号,表明有子进程已经结束,需要进行资源回收等后续处理。操作系统可以通过检测僵尸进程的存在,来触发相应的资源回收机制,确保系统资源能够被及时、正确地释放和重新分配。虽然僵尸进程本身也占用一定的系统资源(主要是 PCB 所占用的内存空间),但这是为了实现进程间的正确通信和资源管理所付出的一种暂时的代价。




1.3、option参数
option设置为0,表示父进程阻塞等待子进程退出;
因为是非阻塞等待。所以使用这个option时一般都要套一层循环;
非阻塞等待的优点:相比于阻塞等待,非阻塞等待在等待的同时可以让父进程进行其他任务,因为程序不会阻塞,所以其他的任务也能执行;这样就提高了效率,这个效率提高的不是子进程退出的效率,而是等待子进程退出前,父进程能够并发地完成其他任务;
代码:
四、进程程序替换
1、快速认识进程程序替换
通过一段代码认识程序进程替换:
程序进程替换就是把excel之后的代码程序进行覆盖式替换,替换的内容是excel里面的内容,替换之后之前代码程序就不存在了,执行的是替换之后的程序;
在替换的过程中,没有创建新的进程,只是代码替换;
excel没有成功返回值,只有失败返回值,但是也用不到返回值,因为若是替换之后执行到后面程序了,那么一定失败;
替换的是代码段和数据段的内容
看看替换失败的例子:
2、程序替换的接口
NAME
execl, execlp, execle, execv, execvp, execvpe - execute a fileSYNOPSIS
#include <unistd.h>extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
execl及其引出的相关概念:
execl,l表示列表的意思,path表示替换程序的路径,后面的...表示多参数,对应execl中的l列表;路径指明要执行谁,多参数指明要怎么执行
命令行怎么传那么execl就怎么写,但是必须要以NULL结尾,表示参数传递完成
通过这个接口来理解更多概念:
子进程替换之后,代码也发生写实拷贝
将execl放在子进程中观察现象:
观察到子进程发生程序替换之后,父进程在替换之后的程代码还是可以执行;
输出结论:程序替换的时候不仅仅是数据,子进程的代码也会发生写实拷贝
替换可以替换我们自己写的程序
编写一段c++代码,在此时的c程序中的子进程使用程序替换,替换的内容为编写的c++代码,观察运行结果:
还可以替换为python,shell脚本语言,这是因为操作系统具有语言无关性(底层这些语言都被处理为了二进制数据)
程序替换不会创建新的进程
结果显示两个程序的pid一样,都是同一个进程,也就是说,程序替换的时候没有创建新的子进程
其他接口:
int execlp(const char *file, const char *arg, ...);
file表示替换之后进行运行的程序,只需要传文件即可;p表示path,也就是这个接口会使用环境变量PATH来找到指定文件的路径
int execv(const char *path, char *const argv[]);
path表示要运行的文件所在的路径,argv是一个指针数组,execv中的v表示vector;使用这个接口是,需要传一个指针数组
这个数组也可以写在父进程里面,那么就是父进程传命令行参数给子进程了;这也是main函数的命令行参数的机制,调用相关程序替换的接口来传参;
int execvp(const char *file, char *const argv[]);和上面一样
int execvpe(const char *file, char *const argv[], char *const envp[]);
env表示传的环境变量,带e的exec系类的接口传环境变量时是覆盖式的传,也就是看不到从父进程拷贝过来的环境变量了
想要不覆盖式的传:
1、使用不带e的接口,此时不传env,子进程拷贝命令行参数环境变量,这个是放在内核数据结构中的,而子进程创建需要先拷贝父进程部分来创建内核数据结构;
所以不传env时,使用的就是父进程的env即完整的环境变量
若是想要在此基础上新增环境变量,则使用putenv接口:
2、若是想使用带e的接口,那么env传environ,此时子进程的环境变量就是全的,当然也可以使用putenv新增
int execve(const char *filename, char *const argv[], char *const envp[]);只有这个才是系统调用,其他的都是语言封装
五、自定义shell的编写
1、打印命令行提示符
2、获取命令行字符串
在命令行运行一下:
3、命令行解析
通过strtok将命令行字符串按照空格分开
g_argc--才能表示分割后命令的个数,因为会包含一个NULL
4、执行
测试:
将执行阶段放在函数里面:
获取到目录而不是整个路径:
看下面一个现象:
使用cd命令时无法更改父进程的路径:
这是因为执行时是子进程执行的,而打印命令行提示符时,打印的是父进程从bash那通过环境变量读来的;那么此时cd时改变的是子进程的路径而父进程没有改变;下一次循环子进程还是继承的父进程的路径,所以cd的结果不变
那么如何解释ls能够达到正常效果呢?因为在子进程中继承了环境变量,所以父进程通过程序替换函数接口将ls一类的字符串传进去的时候,这些环境变量能够帮助子进程运行,运行的结果显示在bash命令行
5、处理内建命令
若是cd命令,cd之后查看到的还是bash的路径,不会改变;此时需要处理;
处理方法:在子进程开始执行之前,处理cd命令,首先判断是不是cd,然后使用chdir改变当前父进程的路径,这样子进程也会变成相应的路径;
注意只有cd时,默认回到家目录
命令行解析失败就重新循环,打印命令行提示以及输入命令字符串;内建命令cd返回值为true则也重新循环,因为此时是通过chdir改变的,不需要输出什么。
但是有一个问题,就是命令行提示器的路径没有随着cd改变;这是因为当前工作路径虽然被cd改变了,但是环境变量没有改变,而命令行提示器的路径来自于我们写的GetPwd(也就是通过PWD环境变量来的);在shell中,我们使用cd改变工作路径时,环境变量也会改变;所以这里要改变一下GetPwd的逻辑,不能每次获取的都是环境变量中的PWD,要根据当前路径来获取pwd从而改变命令行提示符显示的路径:
要使用系统调用getcwd获取当前工作路径,给pwd再返回
但是上述操作都没有改变环境变量,此时输入env发现环境变量没有改变;那么也要改一下环境变量:
将cwd当前工作路径格式化为"PWD=%s"再写入一张cwd的环境变量表,然后使用putenv将cwdenv中的"PWD=当前工作路径字符串"导入环境变量表中
内建命令echo的实现
echo打印字符串、$?即上一个进程的退出码、$环境变量名打印出这个环境变量的具体内容
这里要设置一个全局变量lastexit即上一次的退出码:
6、初始化环境变量表
自己写一张环境变量表;环境变量表应该从父shell统一而来
7、完整代码
1 #include<iostream>2 #include<cstdio>3 #include<cstring>4 #include<cstdlib>5 #include<unistd.h>6 #include<sys/types.h>7 #include<sys/wait.h>8 #define MAXARGC 1289 //命令行参数表10 char* g_argv[MAXARGC];11 int g_argc = 0;12 //环境变量表13 char* g_env[100];14 int g_envs = 0;15 //1、命令行提示符打印16 #define FORMAT "[%s@%s %s]# "17 #define COMMAND_SIZE 102418 19 const char* GetHome()20 {21 const char* home = getenv("HOME");22 return home == NULL ? "" : home;23 }24 const char* GetUsername()25 {26 const char* username = getenv("USER"); 27 return username == NULL ? "None" : username;28 }29 const char* GetHostname()30 {31 const char* hostname = getenv("HOSTNAME");
32 return hostname == NULL ? "None" : hostname;33 }34 char cwd[1024];35 char cwdenv[1024];36 const char* GetPwd()37 {38 //const char* pwd = getenv("PWD");39 const char* pwd = getcwd(cwd , sizeof(cwd));40 if(pwd != NULL)41 {42 snprintf(cwdenv , sizeof(cwdenv) , "PWD=%s" , cwd);43 putenv(cwdenv);44 }45 return pwd == NULL ? "None" : pwd;46 }47 std::string GetDir(const char* pwd)48 {49 std :: string dir = pwd;50 if(dir == "/") return "/";51 size_t pos = dir.rfind("/");52 if(pos == std::string::npos) return "BUG?";53 return dir.substr(pos+1);54 }55 void MakeCmdPrompt(char* prompt , int size)56 { 57 //将字符串格式化写入prompt中58 snprintf(prompt , size , FORMAT , GetUsername() , GetHostname() ,GetDir(GetPwd()).c_str());59 }
void PrintCmdPrompt()61 {62 char cmd_prompt[COMMAND_SIZE];63 MakeCmdPrompt(cmd_prompt , COMMAND_SIZE);64 printf("%s" , cmd_prompt);65 fflush(stdout);66 }67 //2、获取命令行字符串68 bool GetCommandLine(char* out , int size)69 {70 char* c = fgets(out , size , stdin);71 if(c == NULL) return false;72 out[strlen(out)-1] = 0;//消除回车影响73 if(strlen(out) == 0) return false;74 return true;75 }76 77 //3、命令行解析
W> 78 bool CommandParse(char* commandline , int size)79 {80 #define SEP " "81 g_argc = 0;82 g_argv[g_argc++] = strtok(commandline , SEP);83 while((bool)(g_argv[g_argc++] = strtok(NULL , SEP)));84 g_argc--; 85 return g_argc > 0 ? true : false; 86 }
88 void PrintArgv()89 {90 for(int i = 0 ; i < g_argc ; i++)91 {92 printf("argv[%d] -> %s\n" , i , g_argv[i]);93 }94 }95 //4、处理内建命令96 int lastexit = 0;97 bool Dealbuildin()98 {99 std::string cwd = g_argv[0];100 if(cwd == "cd")101 {102 if(g_argc == 1)103 {104 //回到家目录105 std::string home = GetHome(); 106 if(home == "") return true;107 chdir(home.c_str());108 }109 else 110 {111 std::string where = g_argv[1];112 chdir(where.c_str());113 } 114 return true;115 }116 else if(cwd == "echo")117 {118 std::string cont = g_argv[1];
119 if(cont == "$?")120 {121 //获取上一次进程退出码,因为执行程序阶段已经记录,所以直接获得122 std::cout<<lastexit<<std::endl;123 lastexit = 0;//每次打印完就置为0124 return true;125 }126 if(cont[0] == '$')127 {128 //获取环境变量129 std::string str = cont.substr(1);//以string形式获取$之后的字符串 130 const char* e = getenv(str.c_str());131 std::cout<<e<<std::endl;132 return true;133 }134 else 135 {136 std::cout<<cont<<std::endl;137 return true;138 }139 }140 return false;141 }142 //5、执行程序143 void execute() 144 {145 pid_t id = fork();146 if(id == 0)147 {148 execvp(g_argv[0] , g_argv);
149 }150 int status = 0;151 pid_t rid = waitpid(id , &status , 0);152 if(rid > 0)153 {154 lastexit = WEXITSTATUS(status);155 }156 }157 //6、初始化环境变量表158 void InitEnv()159 {160 extern char** environ;161 g_envs = 0;162 memset(g_env , 0 , sizeof(g_env));163 164 //环境变量本来要从配置文件中来165 //获取环境变量166 for(int i = 0; environ[i]; i++)167 {168 g_env[i] = (char*)malloc(strlen(environ[i]) + 1);169 strcpy(g_env[i] , environ[i]);170 g_envs++;171 }172 //测试是不是我们初始化的表173 g_env[g_envs++] = (char*)"FOR_TEST"; 174 g_env[g_envs] = NULL;175 176 //导成环境变量177 for(int i = 0; i < g_envs; i++)178 {putenv(g_env[i]);180 }181 environ = g_env;182 }183 int main()184 {185 InitEnv();186 while(1)187 {188 //1、打印命令行提示符189 PrintCmdPrompt();190 //2、获取命令行字符串191 char commandline[COMMAND_SIZE];192 if(!GetCommandLine(commandline , COMMAND_SIZE))193 continue;194 //printf("%s\n" , commandline);195 //3、命令行解析196 if(!CommandParse(commandline , COMMAND_SIZE))197 continue;198 // PrintArgv();// 4、处理内建命令200 if( Dealbuildin() )201 continue;202 // 5、执行阶段203 execute();204 }205 return 0;206 }