Linux --进程控制
本文从以下五个方面来初步认识进程控制:
目录
进程创建
进程终止
进程等待
进程替换
模拟实现一个微型shell
进程创建
在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。
当进程调用fork以后就会进行分流,内核会分配新的内存块和内核数据结构给子进程,将父进程的部分数据结构内容拷贝到子进程,添加子进程到系统的进程列表之中。
通常,⽗⼦代码共享,⽗⼦在不写⼊时,数据也是共享的,当任意⼀⽅试图写⼊,便以写时拷⻉的⽅式各⾃⼀份副本。因为有写时拷⻉技术的存在,所以⽗⼦进程得以彻底分离离!完成了进程独⽴性的技术保证!写时拷⻉,是⼀种延时申请技术,可以提⾼整机内存的使⽤率,具体⻅下图
fork之前⽗进程独⽴执⾏,fork之后,⽗⼦两个执⾏流分别执⾏。注意,fork之后,谁先执⾏完 全由调度器决定。fork的返回值在父子进程之中有不同的体现,父进程中会返回子进程的pid,子进程返回0,如果fork失败返回-1,这里我们用一段代码解释上面提到的一些概念。
这里我们定义了一个全局变量gav,在没有fork以前我们会看到父进程的pid和gav等于100,fork以后我们通过不同的返回值能够达到不同分支完成不同任务的效果,这里能看到子进程已经将gav修改成了110,而父进程的gav没有改变,两个进程的gav地址一样是因为在各自的虚拟内存的地址,实际它们的物理地址已经发生了写时拷贝被分配了新的物理地址。
需要注意的一点是,子进程会拷贝父进程的的代码,所以在结束if()分支后两个进程都会执行剩下的代码,而不要误解成为剩下的代码会由父进程执行。
fork的常规用法:
⼀个⽗进程希望复制⾃⼰,使⽗⼦进程同时执⾏不同的代码段。例如,⽗进程等待客⼾端请求,⽣成⼦进程来处理请求。
⼀个进程要执⾏⼀个不同的程序。例如⼦进程从fork返回后,调⽤exec函数,这个在进程替换的时候会讲解。
进程终止
进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
进程退出有三个场景:1.代码运行完毕,结果正确。2.代码运行完毕,结果不正确。3.代码异常终止,返回异常信号。
进程终止有三个方式:1.main()函数return 2.调用exit(),可以在代码任何地方,表示进程结束。 3._exit(),这是系统调用接口,exit()的就是底层调用这个接口。这三种方式结束进程都会有返回值,异常终止的进程只有异常信号,这些都会存在内核数据中,父进程可以等待获取。在linux命令行中我们可以使用echo $?来查看进程的退出码。注意这个退出码是程序员自己约定好的,一般来说进程成功运行返回0,其他非0返回则代表进程执行出现错误,异常信号是由系统中断进程再返回的。
Linux Shell 中的主要退出码:



进程等待
为什么要进行进程等待必要性?之前讲过,⼦进程退出,⽗进程如果不管不顾,就可能造成‘僵⼫进程’的问题,进⽽造成内存 泄漏。 另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,“杀⼈不眨眼”的kill -9 也⽆能为⼒,因为谁也没有办法杀死⼀个已经死去的进程。最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如⼦进程运⾏完成,结果对还是不对,或者是否正常退出。 ⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息。、
进程等待通常有两种方式,一个是pid_t wait(int* status),等待成功会返回进程Pid,失败则返回-1,status是一个输出型参数,如果不关心子进程退出状态可以将参数设为NULL。
另外一个是pid_ t waitpid(pid_t pid, int *status, int options)了
返回值: 当正常返回的时候waitpid返回收集到的⼦进程的进程ID; 如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集0,则返回; 如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;
这里我们用一个子进程提前结束但是没有及时被父进程等待回收的例子来说明wait
pid_t id = fork();if(id == 0){int cnt = 5;while(cnt--){cout<<"子进程 pid:"<<getpid()<<endl;sleep(1);}exit(0);}else if(id>0){sleep(10);pid_t rid;rid = wait(nullptr);if(rid>0){cout<<"等待子进程成功 rid : "<<rid<<endl;}sleep(10);}
当子进程结束以后父进程还需要等待5秒才会回收,此时子进程是僵尸状态,五秒以后等待成功,子进程被释放只有父进程
wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。





vector<int> data;
void backup_data(vector<int>& data)
{string name =to_string( time(nullptr));//name += ".backup";FILE* fp = fopen(name.c_str(),"w");//以时间戳命名备份文件if(fp == nullptr)exit(1);string numdata;for(auto d : data){numdata+=to_string(d);//将数据转换为string类型写入文件numdata+=' ';}fputs(numdata.c_str(),fp);fclose(fp);exit(0);
}
int main()
{int num = 0;while(++num){data.push_back(num);//插入数据if(num %10 == 0)//每增加十次数据则进行一次备份{int status = 0;pid_t id = fork();if(id == 0){backup_data(data);//子进程进行备份}pid_t rid = wait(&status);if(rid > 0 && WIFEXITED(status) && WEXITSTATUS(status) == 0)//等待子进程成功并且正常退出(退出码为0){cout<<"备份成功!"<<endl;}else {cout<<"备份失败请注意!"<<endl;}}sleep(1);}
}
运行结果:

进程替换
fork() 之后,⽗⼦各⾃执⾏⽗进程代码的⼀部分如果⼦进程就想执⾏⼀个全新的程序就需要进程的程序替换来完成这个功能。程序替换是通过特定的接⼝,加载磁盘上的⼀个全新的程序(代码和数据),加载到调⽤进程的地址空间中。
替换原理:⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程往往要调⽤⼀种exec函数以执⾏另⼀个程序。当进程调⽤⼀种exec函数时,该进程的⽤⼾空间代码和数据完全被新程序替换,从新程序的启动例程开始执⾏。调⽤exec并不创建新进程,所以调⽤exec前后该进程的id并未改变。这里用一个简单代码可以体现
其实有六种以exec开头的函数,统称exec函数:

char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使⽤环境变量PATH,⽆需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要⾃⼰组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使⽤环境变量PATH,⽆需写全路径
execvp("ps", argv);
// 带e的,需要⾃⼰组装环境变量
execve("/bin/ps", argv, envp);

模拟实现一个微型shell
所以我们利用上诉的几个理论知识就可以手搓一个简易版的shell,我们首先要知道shell的工作原理其实就是:
1.获取命令行 2.解析命令行 3.执行命令
在此之前我们需要先实现shell循环打印我们的信息框 比如:[toutie40@VM-8-16-centos repos]$,这个信息框是由 [+用户名+@+主机名+当前地工作目录]
此时就可以打印处理命令行信息了
接下来我们需要实现地是获取命令行
此时我们就可以获取用户输入地命令了
然后我们需要将获取到地命令进行解析,将comman_buffer的字符分割到gargv的每一个元素中
接着就可以正确解析命令了
下一步就是将我们解析的命令使用子进程调用需要的进程即可
然后我们就可以实现一个简易版的shell了!
但是它只能够正常调用一些系统,cd命令并没有调用成功,或者说调用了但是没有起效,这是因为调用的cd其实是更改了子进程的工作目录,并不能影响我们父进程shell的工作目录,所以这部分需要shell本身调用自身函数来实现功能的命令我们成为自建命令,那么我们可以使用chdir进行更改当前的工作目录,记得要将环境变量中的pwd一起修改,因为Linux中的cd命令也是这样实现的。
通过检查解析后的命令是否与内建命令相同,如果相同则shell本身调用函数实现,命令行的pwd也要记得修改
env,export等命令也是内建命令,我们在系统中export会添加加属于shell维护的env表里面,我们自己写的shell目前的环境变量表是直接继承shell进程的,我们应该也要维护自己的env表,那么这里用系统shell进程环境变量来创建我们自己的env表
使用了自定义的环境变量表以后记得修改命令行获取pwd的使用自定义的环境变量表,替换程序也可以使用我们自定义的环境变量表
至此我们就完成了一个"手搓的"shell,这个shell遵循了系统中shell的基本原理,有自己的环境变量表,能够实现一般命令和内建命令。通过这个案例我们能够知道为什么在export环境变量是一个内存级的修改,因为在每次重新启动shell以后就会重新加载环境变量,而环境变量是以参数的方式传给我们的替换程序从而实现全局变量。
总结
