Linux内核-进程之fork、vfork和clone

2024-05-04 23:38

本文主要是介绍Linux内核-进程之fork、vfork和clone,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

     各种大神的混合,做个笔记。

                http://blog.sina.com.cn/s/blog_7598036901019fcg.html

                          http://blog.csdn.net/kennyrose/article/details/7532912

                           http://blog.sina.com.cn/s/blog_92554f0501013pl3.html

                          http://www.cnblogs.com/peteryj/archive/2007/08/05/1944905.html

进程的四大要素:

Linux进程所需具备的四要素:
     (1)程序代码。代码不一定是进程专有,可以与其它进程共用。
     (2)系统堆栈空间,这是进程专用的。
     (3)在内核中维护相应的进程控制块。只有这样,该进程才能成为内核调度的基本单位,接受调度。并且,该结构也记录了进程所占用的各项资源。
     (4)有独立的存储空间,表明进程拥有专有的用户空间。

       以上四条,缺一不可。如果缺少第四条,那么就称其为“线程”。如果完全没有用户空间,称其为“内核线程”;如果是共享用户空间,则称其为“用户线程”。


do_fork函数

  Linux的用户进程不能直接被创建出来,因为不存在这样的API。它只能从某个进程中复制出来,再通过exec这样的API来切换到实际想要运行的程序文件

      复制的API包括三种:fork、clone、vfork。

      这三个API的内部实际都是调用一个内核内部函数do_fork,只是填写的参数不同而已

do_fork的实现源码在kernel/fork.c文件中,其主要的作用就是复制原来的进程成为另一个新的进程,它完成了整个进程的创建过程。do_fork()的实现主要由以下5个步骤:
(1)首先调用copy_process()函数,copy_process函数实现了进程的大部分拷贝工作。 对传入的clone_flag进行检查, 为新进程创建一个内核栈、thread_info结构和task_struct;其值域当前进程的值完全相同(父子进程的描述符此时也相同);根据clone的参数标志,拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间;为新进程获取一个有效的PID,调用pid = alloc_pidmap();紧接着使用alloc_pidmap函数为这个新进程分配一个pid。由于系统内的pid是循环使用的,所以采用位图方式来管理,用每一位(bit)来标示该位所对应的pid是否被使用。分配完毕后,判断pid是否分配成功。
(2)init_completion(&vfork);
(3)检查子进程是否设置了CLONE_STOPPED标志。
(4)检查CLONE_VFORK标志被设置
(5)返回pid


clone函数

                 int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);

      这里fn是函数指针,我们知道进程的4要素,这个就是指向程序的指针,就是所谓的“剧本", child_stack明显是为子进程分配系统堆栈空间(在linux下系统堆栈空间是2页面,就是8K的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块task_struct的值),flags就是标志用来描述你需要从父进程继承那些资源, arg就是传给子进程的参数)。下面是flags可以取的值:

标志                   含义

 CLONE_PARENT  创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”

 CLONE_FS          子进程与父进程共享相同的文件系统,包括root、当前目录、umask

 CLONE_FILES     子进程与父进程共享相同的文件描述符(file descriptor)表

 CLONE_NEWNS  在新的namespace启动子进程,namespace描述了进程的文件hierarchy

 CLONE_SIGHAND  子进程与父进程共享相同的信号处理(signal handler)表

 CLONE_PTRACE  若父进程被trace,子进程也被trace

 CLONE_VFORK    父进程被挂起,直至子进程释放虚拟内存资源

 CLONE_VM          子进程与父进程运行于相同的内存空间

 CLONE_PID         子进程在创建时PID与父进程一致

 CLONE_THREAD   Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群

内核线程是由kernel_thread(  )函数在内核态下创建的,这个函数所包含的代码大部分是内联式汇编语言,但在某种程度上等价于下面的代码:

 

int kernel_thread(int (*fn)(void *), void * arg,

                  unsigned long flags)

{

    pid_t p;

    p = clone( 0, flags | CLONE_VM );

    if ( p )       

        return p;

    else {         

        fn(arg);

        exit(  );

   }

}


fork函数(unistd.h)

       由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程 id。将子进程id返回给父进程的理由是:因为一个进程的子进程可以多于一个,没有一个函数使一个进程可以获得其所有子进程的进程id。 对子进程来说,之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;也可以调用getppid()来获取父进程的id。

fork创建一个进程时,子进程只是完全复制父进程的资源,复制出来的子进程有自己的task_struct结构和pid,但却复制父进程其它所有的资源例如,要是父进程打开了五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方。所以,这一步所做的是复制。这样得到的子进程独立于父进程, 具有良好的并发性,但是二者之间的通讯需要通过专门的通讯机制,如:pipe,共享内存等机制, 另外通过fork创建子进程,需要将上面描述的每种资源都复制一个副本。但由于现在Linux中是采取了copy-on-write(COW写时复制)技术,为了降低开销,fork最初并不会真的产生两个不同的拷贝,因为在那个时候,大量的数据其实完全是一样的。写时复制是在推迟真正的数据拷贝。若后来确实发生了写入,那意味着parent和child的数据不一致了,于是产生复制动作,每个进程拿到属于自己的那一份,这样就可以降低系统调用的开销。

指令指针也完全相同,子进程拥有父进程当前运行到的位置(两进程的程序计数器pc值相同,也就是说,子进程是从fork返回处开始执行的),但是父子进程谁先被调用就得看操作系统的调度程序了。]

创建了一个子进程之后,父进程和子进程争夺CPU,抢到CPU者执行,另外一个进程等待挂起。如果想要父进程等待子进程执行完后再执行,可以在fork操作之后调用wait或waitpid。

子进程会继承父进程的很多属性:包含用户ID、组ID、当前工作目录、根目录、打开的文件、创建文件时使用的屏蔽字、信号屏蔽字、上下文环境、共享的存储段、资源限制等。但是子进程不会继承父进程设置的文件锁、父进程设置的警告,同时子进程未决信号被清空。


范例1:

int main(){ int num = 1;  int child;  if(!(child = fork())) {   printf("This is son, his num is: %d. and his pid is: %d\n", ++num, getpid());  } else {  printf("This is father, his num is: %d, his pid is: %d\n", num, getpid());  }  
} 
执行结果为:

       This is son, his num is: 2. and his pid is: 2139

       This is father, his num is: 1, his pid is: 2138

从代码里面可以看出2者的pid不同,子进程改变了num的值,而父进程中的num没有改变。

总结:优点是子进程的执行独立于父进程,具有良好的并发性。缺点是两者的通信需要专门的通信机制,如pipe、fifo和system V等。有人认为这样大批量的复制会导致执行效率过低。其实在复制过程中,子进程复制了父进程的task_struct,系统堆栈空间和页面表,在子进程运行前,两者指向同一页面。而当子进程改变了父进程的变量时候,会通过copy_on_write的手段为所涉及的页面建立一个新的副本。因此fork效率并不低。

   

注:fork创建子进程时,子进程继承了父进程父进程的全局变量和局部变量。即不管是全局变量还是局部变量,子进程与父进程的修改互不影响。


范例2:

#include <unistd.h>   
#include <stdio.h>   
int main(void)   
{   int i=0;   for(i=0;i<3;i++){   pid_t fpid=fork();   if(fpid==0)   printf("son/n");   else   printf("father/n");   }   return 0;   } 

这里就不做详细解释了,只做一个大概的分析。
    for        i=0                  2
              father     father     father
                                        son
                            son       father
                                        son
               son       father     father
                                        son
                            son       father
                                        son
    其中每一行分别代表一个进程的运行打印结果。
    总结一下规律,对于这种N次循环的情况,执行printf函数的次数为2*(1+2+4+……+2N-1)次,创建的子进程数为1+2+4+……+2N-1个。

范例3:

#include <unistd.h>   
#include <stdio.h>   
int main() {   pid_t fpid;//fpid表示fork函数返回的值   //printf("fork!");   printf("fork!/n");   fpid = fork();   if (fpid < 0)   printf("error in fork!");   else if (fpid == 0)   printf("I am the child process, my process id is %d/n", getpid());   else   printf("I am the parent process, my process id is %d/n", getpid());   return 0;   
}  
执行结果如下:
    fork!
    I am the parent process, my process id is 3361
    I am the child process, my process id is 3362 
    如果把语句printf("fork!/n");注释掉,执行printf("fork!");
    则新的程序的执行结果是:
    fork!I am the parent process, my process id is 3298
    fork!I am the child process, my process id is 3299 
    程序的唯一的区别就在于一个/n回车符号,为什么结果会相差这么大呢?
    这就跟printf的缓冲机制有关了,printf某些内容时,操作系统仅仅是把该内容放到了stdout的缓冲队列里了,并没有实际的写到屏幕上。但是,只要看到有/n 则会立即刷新stdout,因此就马上能够打印了。
    运行了printf("fork!")后,“fork!”仅仅被放到了缓冲里,程序运行到fork时缓冲里面的“fork!”  被子进程复制过去了。因此在子进程度stdout缓冲里面就也有了fork! 。所以,你最终看到的会是fork!  被printf了2次!!!!
    而运行printf("fork! /n")后,“fork!”被立即打印到了屏幕上,之后fork到的子进程里的stdout缓冲里不会有fork! 内容。因此你看到的结果会是fork! 被printf了1次!!!!

范例4:

int main(int argc, char* argv[])   
{   fork();   fork() && fork() || fork();   fork();   printf("+/n");   }   

答案是总共20个进程,除去main进程,还有19个进程。


范例:5:

创建一个孤儿进程,所谓孤儿进程就是父进程先与子进程结束,子进程就称为一个孤儿进程,它由init进程收养。

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{pid_t pid;pid = fork();switch(pid){case 0:while(1){printf("A background process");sleep(3);//睡几秒让父进程结束 }case -1:perror("Failed");break;default:  printf("I am parent process");exit(0);}return 0;
}


vfork函数

vfork系统调用不同于fork,用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。

因此,上面的例子如果改用vfork()的话,那么两次打印a,b的值是相同的,所在地址也是相同的。

但此处有一点要注意的是用vfork()创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,而fork()则不存在这个情况。

Vfork也是在父进程中返回子进程的进程号,在子进程中返回0。

用 vfork创建子进程后,父进程会被阻塞直到子进程调用exec(exec,将一个新的可执行文件载入到地址空间并执行之。)或exit。vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的 ,因此通过vfork共享内存可以减少不必要的开销。使用vfork创建一个子进程时,保证紫禁城先运行,当子进程调用exit或exec之后,父进程才被调度执行。如果在调用exit或exec之前子进程要依赖于父进程的某个行为,就会导致死锁。

int main() {  int num = 1;  int child;  if(!(child = vfork())) {   printf("This is son, his num is: %d. and his pid is: %d\n", ++num, getpid());  } else {  printf("This is father, his num is: %d, his pid is: %d\n", num, getpid());  }  
}

This is son, his num is: 2. and his pid is:4139
This is father, his num is: 2, his pid is: 4138

从运行结果可以看到vfork创建出的子进程(线程)共享了父进程的num变量,这一次是指针复制,2者的指针指向了同一个内存。

总结:当创建子进程的目的仅仅是为了调用exec()执行另一个程序时,子进程不会对父进程的地址空间又任何引用。因此,此时对地址空间的复制是多余的,通过vfork可以减少不必要的开销。


进程执行新程序

      用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其 main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。有六种不同的exec函数可供使用,它们常常被统称为exec函数。这些exec函数都是UNIX进程控制原语。用fork可以创建新进程,用exec可以执行新的程序。exit函数和两个wait函数处理终止和等待终止。

      首先要理解main函数的两种形式:

               int main(int argc,char **argv);

               int main(int argc,char **argv, char **envp);//envp是环境变量的意思。请将这些参数与下面的这些函数对应起来理解。

exec函数族是一组函数,一共有6个,它们可以执行二进制的可执行文件,也可以执行shell脚本程序。
#include <unistd.h>
extern char **environ;
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[]);
其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。



这篇关于Linux内核-进程之fork、vfork和clone的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/960345

相关文章

Linux join命令的使用及说明

《Linuxjoin命令的使用及说明》`join`命令用于在Linux中按字段将两个文件进行连接,类似于SQL的JOIN,它需要两个文件按用于匹配的字段排序,并且第一个文件的换行符必须是LF,`jo... 目录一. 基本语法二. 数据准备三. 指定文件的连接key四.-a输出指定文件的所有行五.-o指定输出

Linux jq命令的使用解读

《Linuxjq命令的使用解读》jq是一个强大的命令行工具,用于处理JSON数据,它可以用来查看、过滤、修改、格式化JSON数据,通过使用各种选项和过滤器,可以实现复杂的JSON处理任务... 目录一. 简介二. 选项2.1.2.2-c2.3-r2.4-R三. 字段提取3.1 普通字段3.2 数组字段四.

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

Linux云服务器手动配置DNS的方法步骤

《Linux云服务器手动配置DNS的方法步骤》在Linux云服务器上手动配置DNS(域名系统)是确保服务器能够正常解析域名的重要步骤,以下是详细的配置方法,包括系统文件的修改和常见问题的解决方案,需要... 目录1. 为什么需要手动配置 DNS?2. 手动配置 DNS 的方法方法 1:修改 /etc/res

Linux创建服务使用systemctl管理详解

《Linux创建服务使用systemctl管理详解》文章指导在Linux中创建systemd服务,设置文件权限为所有者读写、其他只读,重新加载配置,启动服务并检查状态,确保服务正常运行,关键步骤包括权... 目录创建服务 /usr/lib/systemd/system/设置服务文件权限:所有者读写js,其他

Linux下利用select实现串口数据读取过程

《Linux下利用select实现串口数据读取过程》文章介绍Linux中使用select、poll或epoll实现串口数据读取,通过I/O多路复用机制在数据到达时触发读取,避免持续轮询,示例代码展示设... 目录示例代码(使用select实现)代码解释总结在 linux 系统里,我们可以借助 select、

Linux挂载linux/Windows共享目录实现方式

《Linux挂载linux/Windows共享目录实现方式》:本文主要介绍Linux挂载linux/Windows共享目录实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录文件共享协议linux环境作为服务端(NFS)在服务器端安装 NFS创建要共享的目录修改 NFS 配

linux系统中java的cacerts的优先级详解

《linux系统中java的cacerts的优先级详解》文章讲解了Java信任库(cacerts)的优先级与管理方式,指出JDK自带的cacerts默认优先级更高,系统级cacerts需手动同步或显式... 目录Java 默认使用哪个?如何检查当前使用的信任库?简要了解Java的信任库总结了解 Java 信

Linux命令rm如何删除名字以“-”开头的文件

《Linux命令rm如何删除名字以“-”开头的文件》Linux中,命令的解析机制非常灵活,它会根据命令的开头字符来判断是否需要执行命令选项,对于文件操作命令(如rm、ls等),系统默认会将命令开头的某... 目录先搞懂:为啥“-”开头的文件删不掉?两种超简单的删除方法(小白也能学会)方法1:用“--”分隔命

Linux五种IO模型的使用解读

《Linux五种IO模型的使用解读》文章系统解析了Linux的五种IO模型(阻塞、非阻塞、IO复用、信号驱动、异步),重点区分同步与异步IO的本质差异,强调同步由用户发起,异步由内核触发,通过对比各模... 目录1.IO模型简介2.五种IO模型2.1 IO模型分析方法2.2 阻塞IO2.3 非阻塞IO2.4