#include
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程pid,出错返回-1
因为一个父亲可能有多个孩子,所以为了区分孩子,父进程fork的返回值是孩子的进程pid;
一个孩子只有唯一一个父亲,所以fork之后返回0的部分,就是child的部分;
#include
#include
using namespace std;
int global = 10;
int main()
{
pid_t pid = fork();
if(pid==0){
//global = 1;
cout<<"i am child"<<"global = "<<&global<<" : "<<global<<endl;
}
else if(pid >0){
cout<<"i am child"<<"global = "<<&global<<" : "<<global<<endl;
}
else cerr<<"error"<<endl;
return 0;
}
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:


不修改值时:可以看到父子进程的global变量地址和值都一样;
可是子进程修改了这个global变量了,结果如下:

我们发现他们的值确实是不一样了,但是地址还是一样的,如果这个地址是真实的物理内存,这显然是不现实的;
所以这个地址是一个虚拟内存,操作系统为每个进程分配的都是虚拟内存,都在画大饼,每个进程都是0~ffffffff 4G的内存(32位下),真实的物理内存,是通过虚拟内存和页表结合映射才能找到;

OS使用虚拟内存的好处:
//wait函数的功能是等待任意 一个 进程退出,并获取该进程的退出状态。 多线程需要循环调用;
#include
#include
pid_t wait(int*status);
//waitpid函数等待一个进程退出,并拿到status状态,可以设置阻塞or非阻塞options的方式;;WNOHANG非阻塞轮训等待方式的过程中,父进程可以做别的事;
pid_t waitpid(pid_t pid, int *status状态, int options);
//参数:pid>0,等一个确定的子pid,pid=-1 等任意一个子进程;
我们发现,不管是哪个等待函数,都会有一个int*status参数,这其实是一个输出型参数; 是为了父进程拿到子进程的退出码或者终止信号,便于分析子进程的完成状态;
分为正常终止–>退出码exit(x) 和 收到信号异常终止–>SIGN=X:

我们可以用位操作的形式,拿到status高8位的正常退出码,也可以用下列了两个宏方便操作:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零(正常退出),提取子进程退出码。(查看进程的退出码)
if(WIFEXITED(status))//退出信号为0,子进程正常退出
int code = WEXITSTATUS(status);//子进程退出码
core dump标志位,在子进程异常终止后,置为1,我们会发现产生一个新的文件“core”,里面记录了异常原因;
当一个进程要异常终止时,可以选择把用户空间的内存数据全部保存到磁盘上,文件名通常是core,这个过程就叫做core dump。进程异常终止通常是因为有bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因;
有了进程创建,引入进程替换:
一般来说可以创建一个进程,然后替换掉子进程,这样可以保证父进程继续执行常规逻辑的前提下,子进程执行特定的替换程序;
进程替换是不会创建新的进程的!exec系列的函数让特定进程读取加载磁盘中的其他程序;
子进程在创建后会有一块自己的进程地址空间(虚拟内存),同时通过页表映射到物理内存上,而进程替换的原理就是通过改变页表的映射关系,将子进程的进程地址空间映射到要替换的程序所对应的物理内存上即可,此时子进程要执行的程序就被替换了。

而在进程替换的过程中,本质上并没有创建新的进程,因为虽然子进程执行的程序被替换了(修改了页表映射关系),但子进程自己的pid并没有发生变化。
//exec系列进程替换函数
#include
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[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
- 这些函数若执行成功则程序被替换,后续代码不再执行。
- 若调用出错则返回-1。
下列代码展示替换进程:
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
printf("I am child. pid: %d,ppid:%d\n",getpid(), getppid());
execl("test", NULL);//替换接口; NULL代表输入完了命令行参数!
exit(1);//调用失败
}
int status = 0;
int ret = waitpid(-1, &status, 0);
if(WIFEXITED(status))//子进程正常退出
{
int code = WEXITSTATUS(status);//获取子进程退出码
printf("wait successfully, exit code: %d, pid: %d\n", code, ret);
}
else
{
printf("wait failed,sig: %d\n", (status & 0x7F));
}
return 0;
}

可以看到,对于execl函数,第一个参数是要替换的程序所在的路径,其后为可变参数列表,即为在命令行上要执行的命令及选项,这种传参方式叫做以列表方式传参,即list方法;最后需以NULL结尾,告知exec系列函数传参结束。
//execv函数接口就不是以列表的形式传入参数了,而是通过数组(vector)的形式传入参数,当然两者的最终结果并无区别。
char* const my_argv[] = { "ls", "-l", "-a", NULL};
execv("/usr/bin/ls", my_argv);
//有p会自动搜索环境变量PATH(只有系统命令或我们导入到PATH路径下的命令可以搜索),这也就意味着,在第一个参数中对于要替换的指令我们并不需要带上路径,而后面的参数则与execl函数接口的参数一致;
execlp("ls", "ls", "-l", "-a", NULL);
通过上面的内容我们知道程序替换就是通过exec系列的函数让特定进程取加载磁盘中的其他程序,以达到运行的目的,并且期间不创建新的进程。
如果子进程有新的程序需求就需要进程替换。
exec系列函数只要返回了,就说明函数调用失败了。