一个程序
cpp
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
int val=0;
pid_t pid=fork();
if(pid==0)
{
val+=10;
cout<<"我是子进程"<<"pid是"<<" " <<getpid()<<"父进程id是"<<getppid()<<" "<<"val是"<<val<<" " <<"val的地址是"<<&val<<endl;
}
else
{
cout<<"我是父进程"<<"pid是"<<" "<<getpid()<<"val是"<<val<<" "<<"val的地址是"<<&val<<endl;
}
return 0;
}
运行结果

我们发现,父子进程的中val的地址都是一样的,但是打印出来的结果却不一样,原因如下打印的地址是逻辑地址,并非是val的物理地址,在创建子进程的时候,子进程会将父进程的代码数据继承过去,如果数据没有改变,他们的数据会指向同一个地址,但是如果子进程改变了数据,就会发生写时拷贝,逻辑地址不变,但是将实际的数据拷贝一份,放入新的物理地址,图片如下

再来解释一下刚开始的时候,每个进程都有自己的PCB,每一个PCB,也就是一个结构体,当创建一个子进程的时候会将mm_struct拷贝一份,这个时候父子进程数据的页表中的虚拟地址和物理地址都是一样的,但是当子进程改变了数据的话,子进程的数据的物理地址会改变的
进程的创建
我们来探讨一下写时拷贝相关的原理
子进程的创建本质上就是父进程代码和数据的拷贝,当然,页表中的数据也是一样会被拷贝的,页表中除了有虚拟地址到物理地址的映射,还有数据的读写权限,当子进程想要修改数据的时候,发现数据是只有读的权限的时候,会触发错误,系统检测到发生了错误就会判断是否要进行写时拷贝,如果是写时拷贝的话,就会修改页表,申请空间之类的操作
进程的终止
main是有返回值的,我们写代码都是很清楚的,但是为什么要返回要有返回值嘞?这个返回值就是错误码,是用来方便父进程知道子进程的运行情况的,如果返回的是0,就表示这个程序运行成功了,如果是非0的话,就表明这个程序运行失败了,这也是为什么我们的在写main程序的时候是返回0的原因了
下面我们来介绍几个命令
echo $?
这个命令返回的是最近的一个进程结束的错误码

errno和strerror
这两个函数一个是错误码,一个是错误码对应的错误信息,通常在程序里面使用
cpp
#include<iostream>
#include<cstring>
#include<errno.h>
using namespace std;
int main()
{
FILE *fd=fopen("./no_such_file.txt","w");
cout<<"errno"<<errno<<" "<<"错误信息"<<strerror(errno)<<endl;
return 0;
}
运行结果

我们可以来打印一看各种状态码对应的状态信息
cpp
#include<iostream>
#include<cstring>
#include<errno.h>
using namespace std;
int main()
{
for(int i=0;i<=20;i++)
{
cout<<"errno"<<i<<" "<<"状态码"<<strerror(i)<<endl;
}
return 0;
}
运行结果

进程的终止
进程的终止有下面两种方式
1.main函数里面的return 0
2.exit
cpp
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<errno.h>
using namespace std;
void func()
{
cout<<"你好"<<endl;
exit(0);
}
int main()
{
func();
cout<<"hello world"<<endl;
return 0;
}
函数运行结果

从代码的执行情况来看,exit函数无论在哪一层栈帧,只要出现,就会终止整个进程,mian函数里面的hello world都没有打印
和_exit()函数的区别:exit函数在会将缓冲区里面的内容打印在出来,_exit不会
cpp
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<errno.h>
#include<cstdio>
using namespace std;
int main()
{
cout<<"hello world"<<endl;
exit(0);
}
运行打印:hello world
cpp
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<errno.h>
#include<cstdio>
#include<unistd.h>
using namespace std;
int main()
{
printf("hello world/n");
_exit(0);
} //不会打印hello world

进程等待
在父子进程当中,如果子进程运行完毕,但是父进程父进程没有运行完的话,父进程就会卡在哪里去先去回收子进程,类似于scanf函数一样等待用户输入
介绍一个函数
pid_t wait(int *status),父进程要使用这个函数阻塞等待去回收子进程,status返回子进程的状态信息,等待成功返回子进程的pid,失败返回-1
cpp
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<errno.h>
#include<cstdio>
#include<unistd.h>
#include<sys/wait.h>
#include<unistd.h>
using namespace std;
int main()
{
pid_t pid=fork();
if(pid==0)
{
printf("hello world\n");
sleep(5);
exit(5);
}
else
{
int status=0;
wait(&status);
printf("我是父进程\n");
printf("%d",status);
}
exit(0);
}
等待子进程运行完5秒之后父进程才开始运行
waitpid方法
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
1.pid- 指定要等待的子进程
pid > 0:等待进程ID等于pid的特定子进程
pid = -1:等待任意子进程(等同于wait)
pid = 0:等待与调用进程在同一进程组的任意子进程
pid < -1:等待进程组ID等于|pid|的任意子进程2.
status- 存储子进程退出状态
如果不为NULL,存储状态信息
可用宏解析状态:
WIFEXITED(status):是否正常退出
WEXITSTATUS(status):获取退出码
WIFSIGNALED(status):是否被信号终止
WTERMSIG(status):获取终止信号
WIFSTOPPED(status):是否被信号暂停
WSTOPSIG(status):获取暂停信号3.
options- 控制行为选项
0:阻塞等待直到子进程结束
WNOHANG:非阻塞,若无子进程退出立即返回0
WUNTRACED:也返回被暂停的子进程状态
WCONTINUED:也返回被恢复的子进程状态(Linux 2.6.10+)返回值
成功:返回状态变化的子进程ID
失败:返回 -1(设置errno)
WNOHANG且无子进程退出:返回0
cpp
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<errno.h>
#include<cstdio>
#include<unistd.h>
#include<sys/wait.h>
#include<unistd.h>
using namespace std;
int main()
{
pid_t id=fork();
if(id==0)
{
printf("我是子进程,pid是%d\n",getpid());
exit(123); //自定义退出码
}
else
{
int status=0;
pid_t ret=waitpid(id,&status,0); //0:第三个参数为0,阻塞到子进程结束
if(ret==-1) //等待失败
{
perror("waitpid");
exit(1);
}
//子进程正常退出
printf("我是父进程,子进程正常退出,退出码是%d\n",status>>8);
}
return 0;
}
运行结果

在上面打印退出码的时候status需要右移动8位才能得到真正的退出码

因为次高八位才是存储状态码的地方,低八位是退出的信号值
正常退出
8-15位: 退出状态码(0-255)
bits 0-7: 全为 0
被信号终止bits 0-6: 终止信号编号(1-31)
bit 7: 是否产生core dump(通常为1)
bits 8-15: 0
再实际使用中,如果子进程一直没有运行完毕,那需要一直让父进程去等待他吗,答案是否定的,因此我们需要设置第三个参数
cpp
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<errno.h>
#include<cstdio>
#include<unistd.h>
#include<sys/wait.h>
#include<unistd.h>
using namespace std;
int main()
{
pid_t pid = fork();
if (pid == 0)
{
sleep(5);
exit(0);
}
else
{
int status;
pid_t ret;
while ((ret = waitpid(pid, &status, WNOHANG)) == 0) {
printf("子进程仍在运行,执行其他任务...\n");
sleep(1);
}
if (ret == pid)
{
printf("子进程已结束\n");
}
}
运行结果:

这样子进程没有运行结束父进程就可以去做其他的事情了,这叫做非阻塞等待
进程程序替换
下面我们来介绍几个函数
excel
cpp
int execl(const char *path, const char *arg, ...);
参数说明
path:可执行文件的完整路径(如/bin/ls)
arg:可变参数列表,依次传递命令行参数
第一个参数通常是程序名(惯例)
最后一个参数必须是
(char *)NULL作为结束标记返回值:成功不返回(当前进程被替换),失败返回 -1
cpp
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
execl("/bin/ls","ls","-a","-l",nullptr);
return 0;
}
运行结果

注意,这里是程序替换,也就是说,是用参数里面的可执行程序去替代现在的进程

我们还可以用来执行自己的程序
cpp
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
execl("/home/LiHao/practice/test202645/helloworld","helloworld",nullptr);
return 0;
}
其中helloworld是helloworld.c的可执行程序,里面的内容是打印hello world
execlv
cpp
int execv(const char *path, char *const argv[]);
参数 含义 path可执行文件的路径(绝对路径或相对路径)-3-8 argv[]参数数组,第一个元素通常是程序名,最后一个必须是 NULL
比如下面的
cpp
int main()
{
const char *argv[]={"ls","-a","-l",NULL};
execv("/bin/ls",argv);
}
execlp
cpp
int execlp(const char *file, const char *arg, ...);
关键特性
不会返回 :执行成功时,
execlp不会返回,当前进程的代码被新程序完全替换查找路径 :通过
PATH环境变量查找file,无需写绝对路径参数格式:
第一个
arg通常是程序名(可自定义)后面的
arg依次对应命令行参数最后必须 以
(char *)NULL结尾返回值:
成功:不返回
失败:返回
-1,并设置errno
cpp
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
execlp("ls","ls","-a","-l",nullptr);
return 0;
}
运行结果

但是会不会觉得第一个参数和第二个参数有点重复了?不是这样的
第一个参数 :是给内核和操作系统看的,用来定位磁盘上的文件。
第二个参数 :是给新启动的程序看的,是它的"人设"或第一个输入数据。所以,这两个参数是可以不同的
cpp
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
execlp("ls","echo","-a","-l",nullptr);
return 0;
}
我们将第二个参数变成echo,发现运行结果是一样的
execvp
cpp
int execvp(const char *file, char *const argv[]);
这个函数也不需要带绝对路径,只是需要将参数放在一个数组里就行了,其他的返回值之类的和上一个函数是一样的
cpp
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
char *argv[]={"ls","-a","-l",nullptr}; //以nullptr结尾
execvp("ls",argv);
return 0;
}
运行结果

execvpe
这个是带环境变量的,如果需要自己传递环境变量,可以自己定义
cpp
int execvpe(const char *file, char *const argv[],
char *const envp[]);
具体先不举例子了
上面的函数可能都不好记忆:带l的是列表,带v的是vector,带p的是path,带e的是environment环境变量