进程控制——fork和exec
fork
fork是UNIX系统中产生新进程的唯一办法
fork的作用是复制当前进程,生成子进程,子进程从fork的位置继续执行。fork函数的特点概括起来就是“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中 各返回一次。fork就像一个叉子,把顺序执行的程序变成了两个分支,并发执行。
fork调用失败返回-1,子进程中fork的返回值是0,而父进程中fork的 返回值则是子进程的id
fork的用法
- 一个进程创建一个自身的副本,这样每个副本都 可以在另一个副本执行其他任务的同时处理各自的某个操作。 这是网络服务器的典型用法;
- 一个进程想要执行另一个程序。既然创建新进程的唯一办法是调用fork, 该进程于是首先调用fork创建一个自身的副本,然后另一个副本(通常为子进程)调用exec把自身替换成新的程序。 这是shell之类程序的典型用法;
在父进程调用fork以后要用变量记录返回值即子进程的pid,否则就找不到子进程了,而子进程可以通过getppid找到父进程。
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = fork();
if(-1 == pid){
perror("fork");
exit(1);
}
if(0 == pid){
printf("i am parent, %d\n",getpid());
}
else if(pid >0){
printf("i am child, %d\n",getpid());
}
}
i am child, 5909
[root@192 ~]# i am parent, 5910
写时拷贝
一般情况下,子进程和父进程共享地址空间,当有一方需要对数据内容进行修改的话,对各自拷贝一份副本(拷贝修改数据的页表)。由于代码段肯定是不会改变的,所以代码段一直享用同一块地址空间。
int g_val = 0;
int main()
{
pid_t pid = fork();
if(pid == 0){
//子进程
g_val = 100;
printf("child val=%d, &val = %d\n", g_val, &g_val);
}
else{
sleep(3);
printf("parent val=%d, &val = %d\n", g_val, &g_val);
}
}
child val=100, &val = 134518596
parent val=0, &val = 134518596
运行结果说明:
1. 两个变量内容不同,说明两个进程各存有一份变量
2. 两个变量地址一样,说明是虚拟地址
vfork
- 保证了子进程先于父进程执行
- 子进程执行完如果不使用exit,会修改main栈帧,导致父进程段错误
- vfork不存在写时拷贝,效率高
- 通常不使用vfork,有问题
int main()
{
int r = 10;
pid_t pid = vfork();
if(pid == 0){
sleep(1);
r = 1;
printf("child %d\n", r);
exit(0);
}
else{
printf("parent %d\n", r);
}
}
child 1
parent 1
exec
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
exec实现重定向
调用exec后,原来打开的文件描述符仍然是打开的。
解释:在Unix/Linux中,父进程调用fork出一个子进程,父进程打开的文件描述符会被复制到子进程中,这样子进程通过父进程复制过来的文件描述符和父进程共享打开文件的文件表项。
而exec则会继承子进程的文件描述符,也就是说在子进程中打开的文件流都可以在exec后的新进程中使用。