我们用系统调用在代码内部对进程实现相关的操作,包括创建子进程/进程的退出/进程等待/进程替换程序内容执行其他程序。
进程创建
我们知道进程是可以被创建出来的,那么是谁创建了进程?大家可能想到的是被操作系统创建出来的,但只是部分进程由操作系统创建。
当用户登录的时候,操作系统会自动给用户分配一个shell。当用户要执行某个程序的时候,会通过某些操作来完成子进程的创建。同理我们自己写的程序也可以通过这个操作完成子进程的创建
fork创建子进程
通过fork函数来创建子进程
cpp
#include <unistd.h>
pid_t fork(void);
fork会创建子进程,子进程会继承父进程的进程地址空间。相当于两个进程公用一套,具体细节我们下面再讲。
fork会返回pid_t,即进程的ID,当时父进程的时候,会返回子进程的pid,当是子进程的时候,返回0。
cpp
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
using namespace std;
int main(){
cout<<"start"<<endl;
pid_t id = fork();
if(id==0){
cout<<"pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
}
else{
sleep(1);
cout<<"pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
}
return 0;
}

这里也能推出来,后打印的是父进程,先打印的是子进程。而且我们发现start只打印的一次。原因是因为继承后的父子进程只会从fork处往下执行,并不会从头开始执行。
写时拷贝
大家是否有疑问,既然子进程继承了附近的程序地址空间,如果子进程改变某个地址的值,父进程的也会改变吗?
我们测试一下:
cpp
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
using namespace std;
int main(){
cout<<"start"<<endl;
pid_t id = fork();
int x=0;
if(id==0){
x=2;
printf("%p\n",&x);
cout<<"pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
}
else{
sleep(1);
printf("%p\n",&x);
cout<<"x:"<<x<<endl;
cout<<"pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
}
return 0;
}

我们发现并没有改变,而且这是必须的,否则就违背了进程的独立性。进程之间不能相互干扰。在接收pid的时候就已经有体现了,明明子进程是继承的父进程的进程地址空间,为什么会有不一样的值的变量。
但是还是有疑问,他们的地址时一样的啊?
原因是进程地址空间的地址是虚拟地址,子进程进程过去的时候,虚拟地址时直接拷贝过去的,哪个变量存储在同一个虚拟地址上面。但是物理地址不同。这里就设计到了页表,页表就设计到了虚拟地址到物理地址的转换。当创建子进程后,会将原本可写可读的地址权限变为只读的权限,如果有某一方要写当前的物理地址,就会触发中断,为当前进程创建新的物理地址。这样做的原因是为了节省空间。

用法
可以让子进程替父进程做一些事情,这样父进程就可以专注于做一件事。
进程退出
程序在满足某些情况后需要退出
退出场景
代码运行完毕,结果正确 代码运行完毕,结果不正确 代码异常终止
进程常见退出方式
_exit()
不会执行任何操作(例如缓冲区刷新)直接退出,并回收空间
exit()
这个是C++标准库对_exit的封装,它会执行用户注册的函数并刷新缓冲区再退出
注册函数atexit(void(*)())
通过传入一个函数指针来注册程序退出前完成的函数。例如保存相关内容,以免数据丢失等
cpp
#include<iostream>
using namespace std;
void save(){
cout<<"save all txt"<<endl;
}
int main(){
atexit(save) ;
exit(0);
}
最终就会执行save函数
如果使用_exit就不会执行
abort()
会触发对应信号,给进程自身发送退出信号并退出。若开启了ulimit -c unlimited,会生成core.xxx文件,用gdb a.out core.xxx可调试崩溃原因。同时它会跳过用户注册函数和缓冲区刷新。
退出码
退出码(退出状态)可以告诉我们最后⼀次执⾏的命令的状态。在命令结束以后,我们可以知道命令
是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表⽰执⾏成功,没有问题。
代码1 或 0 以外的任何代码都被视为不成功。

进程等待
进程等待的必要性
之前讲过,子进程退出,父进程如果不管不顾,就可能造成"僵尸进程"的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,"杀人不眨眼"的kill-9也无能为力,因为谁也没有办法杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
wait
cpp
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
会直接等待所有的子进程。需要传入一个返回形参数
status

我们知道退出码是8个字节,因此正常终止时,低8位就是0,高8位是退出码;被信号杀时,即异常退出,低七位为终止信号
status提取函数
我们可以用位运算自己提取,但是有现成的:
WIFEXITED(wait if exited)
cpp
#include<sys/types.h>
#include<sys/wait.h>
int WIFEXITED(int status);
判断是否是正常退出的,非零为真
WIFSIGNALED(wait if signaled)
cpp
#include<sys/types.h>
#include<sys/wait.h>
int WIFSIGNALED(int status);
判断是否是信号所杀的,非零为真
WIFSTOPPED(wait if stopped)
cpp
#include<sys/types.h>
#include<sys/wait.h>
int WIFSTOPPED(int status);
判断是否是信号停止的,非零为真
WEXITSTATUS(wait exit status)
获取退出码,范围是0-255
WTERMSIG(wait terminal signal)
获取信号编号,来判断是哪个信号
WSTOPSIG(wit stop signal)
获取信号编号,来判断是哪个信号。因为暂停信号的status位不一样,因此要用这个函数来提取。
WCOREDUMP(wait coredump)
获取coredump的标志位,1表示生成了core dump危机文件,否则表示没有
waitpid
这个比wait用的更广泛,因为它的功能更加强大
cpp
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
pid
pid=-1,表示等待任意一个子进程,和wait等效
pid>0,表示等待指定的子进程
status
上面说过不说了,和wait一样
当我们不想接收状态的时候,可以传空指针
options
options传参是将多个状态位运算在一起
传 0 表示阻塞等待子进程
传 WNOHANG(wait no hang),表示非阻塞等待,不能和0或在一起,因为两个是独立的
传 WUNTRACED(wait un traced),表示接受等待继续信号,只有传了这个才能去接收等待继续的信号。用WIFSTOPPED/WSTOPSIG才有作用
返回值
正常接收返回子进程的pid。失败返回-1。如果设置了WNOHANG,如果没有等待到子进程则会返回0。
总结和举例
说实话这部分用的并不是很多,获取子进程退出状态并提取的情况可能比较少。下面是一个简单的代码

或者是用WNOHANG的,这样可以让父进程边运行边等待:

进程程序替换
既然有了子进程,那么子进程可以为我们干很多事情,特别是在CPU多核的现在,进程是可以并发运行的。因此有子进程可以大大提高运行的效率。因此我们可以让子进程安排任务:进程程序替换就是一个方式:
替换原理
当一个程序调用exec函数的时候,该进程的用户空间代码和数据就完全被新程序所替换,从新程序的启动历程开始执行。调用exec并不会创建新的进程,进程的pid也不会改变。

替换函数
cpp
#include <unistd.h>
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 execve(const char *path, char *const argv[], char *const envp[]);
返回值
当调用成功就没有返回值,因为会被替换成新程序的代码,出错时会返回-1.
命名理解
l(list):表示参数采用列表
v(vector):表示参数采用数组argv
p(path):表示参数自动搜索环境变量下的程序
e(env):表示自己维护环境变量

cpp
#include <unistd.h>
int main()
{
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);
exit(0);
}
简单实现子进程运行命令


自主实现shell命令行解释器
大家看完上面的东西后,应该知道怎么实现了。
环境变量
首先shell创建会在系统文件里面获取环境变量,方便搜寻命令来执行,我们就不那么严格了,直接运行继承真正shell的环境变量就行了。下面是相关操作函数:
setenv
cpp
#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
name代表环境变量名,value是值,overwrite为0表示如果存在不覆盖,1表示存在也覆盖
成功返回0,失败-1
getenv
cpp
#include <stdlib.h>
char *getenv(const char *name);
获取环境变量名
做个测试


为什么要提到环境变量,原因就是要实现内置命令例如cd,export,echo,exit,pwd
工作路径
**这里我们使用setenv对PWD修改来模拟cd是正确的吗?并不正确,正真的工作路径存储在PCB里面,需要我们调用chdir系统调用改变。**因此正确做法是chdir修改工作路径,同时修改PWD的拷贝。
这里挑的几个常用的方便实现的

