前言
unix操作系统提供了一系列进程的控制原语来操作进程,fork()/vfork()就是其中用于创建新进程的两种系统调用。
fork
fork()函数创建的新的子进程是原本父进程的副本,从虚拟地址空间的角度来看,可以用下图表示。此时的子进程几乎和父进程一摸一样,包括代码的执行位置。
fork()函数在unistd.h文件中声明,下面是函数原型:
pid_t fork();
pid_t是进程id的类型,fork()失败时,返回值为-1,fork()成功时,父进程返回子进程的进程id,子进程返回0。下面的代码用于创建一个子进程。
int main(){
pid_t pid =fork();
if(pid<0){
return 0;
}
else if(pid==0){
std::cout << "i am child process, my process id is " << getpid() << " my father process id is " << getppid() << std::endl;
}
else{
std::cout << "i am father process, my process id is " << getpid() << " my father process id is " << getppid() << std::endl;
}
if(waitpid(pid, nullptr, 0)<0){
return 0;
}
return 0;
}
运行结果如下:
虽然上面的运行结果中,父进程先运行,但实际上父进程和子进程的优先级是相同的,它们需要争抢cpu,也就是说父进程和子进程的运行顺序是不确定的。
通常我们创建一个新的进程是为了装载一个新的程序,这可以通过exec函数族来实现,但在装载一个新程序之前,我们需要复制父进程的整个虚拟地址空间,这需要消耗很多资源,为此linux引入了写时复制(copy on write,COW)技术。
写时复制是在fork()执行时,不对虚拟地址空间进程复制,仅复制父进程的页目录表和页表,并将虚拟地址空间的权限从RW(读写)改为(RO)只读,如果此时调用exec函数族加载新的可执行程序,子进程将直接获得新的虚拟地址空间。即使此时没有装载新的可执行程序,如果只是对内存进行读取,也不需要任何新的操作,当需要对内存进行修改时,才需要复制对应虚拟地址空间的地址,复制后父进程权限再从RO修改为RW。
综合上面的叙述,其实在fork()执行后,虚拟地址空间基本没有变化,只有需要写入内存时才修改虚拟地址空间。此外,调用fork创建的子进程需要父进程通过wait()或waitpid()进行回收,否则子进程的pcb资源不能释放,成为僵尸进程。
vfork
vfork()函数也可用于创建一个子进程,与fork()不同的是,vfork()会将父进程挂起,只执行子进程中的逻辑,等待子进程退出后再解除对父进程的阻塞。vfork()本质上是将父进程的虚拟地址空间借用给子进程,子进程使用完毕后归还给父进程,在没有写时复制之前,vfork由于不需要复制虚拟地址空间,相较fork有很大优势。写时复制引入后,vfork()相比fork()欠缺了灵活性,因此现在主要以使用fork为主。
守护进程的创建
守护进程是不依赖于终端的,只在后台运行的进程,通过本文和之前对exec函数族的讨论,可以实现守护进程的创建。可以通过以下几步创建守护进程:
1.父进程通过fork()创建子进程,父进程exit()退出,此时子进程变为孤儿进程,被init进程收养。
2.子进程调用setsid()改变会话属性,担任会话首领。
3.子进程再次fork()并调用exit()退出,这一步是为了使进程不再是会话首进程,避免打开终端。
4.调用chdir是子进程工作路径为根目录。
5.调用umask清除文件掩码。
6.关闭不需要的文件描述符,通常为默认打开的前三个文件描述符。
对应的代码如下:
#include<stdio.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
pid_t pid;
pid=fork();
if(pid>0){
exit(0);
}
setsid();
pid = fork();
if(pid>0){
exit(0);
}
chdir("/");
umask(0);
int i;
for(i=0;i<1024;i++){
close(i);
}
while(1){
/*
... //运行守护进程
*/
}
close(fd);
}