fork()问题详解

       最近在看linux编程方面的书,然后也在网上查阅相关的资料发现了一个关于fork()的几个题,在这里记录一下!
   #include "sys/types.h"
   #include "unistd.h"
   #include pit_t fork(void );
   fork()函数调用成功,返回两个值;
   父进程:返回子进程的PID;
   子进程:返回0;

   出错:返回-1;

        一个进程主要包括以下几个方面的内容:
  (1)一个可以执行的程序
  (2) 与进程相关联的全部数据(包括变量,内存,缓冲区)
  (3)程序上下文(程序计数器PC,保存程序执行的位置)

   这里主要复习一下,fork()的执行和几个需要注意的地方,此外还有几个问题。

第一:COW(Copy-On-Write)写时复制技术,其实在 这里也有讲到,这里再重复一下!
看下面一段程序1:
#include<stdio.h>  
#include<string.h>  
#include<stdlib.h>  
#include<unistd.h>  
int main()  
{  
    int  num=11;  
    pid_t pid=fork();  
    if(pid==0)  
    {  
        num=22';  
        printf("子进程中num=%d\n",num);  
        printf("子进程中num的首地址:%x\n",&num);  
    }  
    else  
    {  
        sleep(1);  
        printf("父进程中num=%d\n",num);  
        printf("父进程中num的首地址:%x\n",&num);  
    }  
    return 0;
} 

运行结果:

fork()问题详解_第1张图片

     从运行结果来看,发现fork()函数确实创建了两个函数,并且每个函数独立运行,变量也不共享,子进程确实复制了父进程的资源,不然num的值会一样。
但是为什么父/子进程指向的num的首地址一样呢,都是0xbfe88098?
     这里涉及到了逻辑地址(虚拟地址)和物理地址,把逻辑地址映射到物理地址我们称为重定向。
逻辑地址:CPU产生的地址(也即虚拟地址),分为页式虚拟存储器,段式虚拟存储器和段页式虚拟存储器;通过基址+偏移地址得到
导物理内存地址。这就依赖于地址变换机构,由专门的MMU完成管理。
物理地址:内存所看到的地址,程序员是看不见真正的物理地址的,只能看见逻辑地址。
静态重定向:在程序装入内存时以完成了逻辑地址到物理地址的变换,在程序执行期间不会发生改变。
动态重定向:在程序的执行期间完成逻辑地址到物理地址的变换。
      说了这么多,这个程序的意思就是:该变量的逻辑地址一样,但是物理地址却不一样(最后输出的值不一样),这是因为fork在创建子进程后并没有完全给子进程分配它所需要的物理内存,而是仅仅复制了虚拟内存空间,其实父/子进程是共享这个物理内存的,当有进程需要改写变量的值的时候,这时候才给改变的这个进程分配相应的物理内存,也就是COW技术。
第二、下面的程序到底创建了几个进程?输出几个“-”?
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
        int i;
	for(i=0; i<3; i++)
        {
	   fork();
	   printf("*\n");  //进程里打印的;
	}
	printf("  +\n"); //代表创建的进程数;
	return 0;
}

运行结果:

fork()问题详解_第2张图片

   运行的结果乍一看可能不太懂,让我慢慢来解释,*号代表每个进程都会打印出一个*,+号代表的是创建的进程数。因为这里的进程不是共享一个变量,所以我想用count计数器来计数,却办不到,因为每个计数器count都是从1开始的,最后还得加起来,看起来更乱,所以我就用*和+来表示更直观。这也证明了一个问题,那就是fork出的函数确实是资源空间的复制而不是指针的复制(vfork)!!很好!

     因为fork是父进程创建一个子进程,所以为什么*会比+多,是因为进程有重复的,但是退出是一定的且只会退出一次,所以+刚好可以代表创建了几个进程。仔细想一想就会明白了。。。

   其实其进程创建的过程是这样的:

  fork()问题详解_第3张图片

数字代表进程号,这就是具体产生的过程。

下面我们可以通过查看其PID看一下创建的进程:

fork()问题详解_第4张图片

    从图中可以看出:共产生了进程6773,6774,6775,6776,6777,6778,6779,6780,这8个进程。

     所以打印14个*,8个+号,到这里应该明白了吧!

    这里还存在一个复制缓冲区的问题:

     如果将上面的输入格式:

         printf("*\n");   
或者:   printf("*");fflush(stdout);

换成:

         printf("*");

       那么结果将打印24个*号!这是因为printf(“*”);语句有buffer,所以,对于上述程序,printf(“*”);把“*”放到了缓存中,并没有真正的输出在fork的时候,有点儿exit()和_exit()的意思!缓存被复制到了子进程空间,所以,就多了10个,就成了24个,而不是14个。这个可以自己动手实践一下就明白了!

  另外,我们知道,unix系统下有“块设备”和“字符设备”之分,所谓块设备就是一块一块的读取数据的设备,如磁盘,内存;字符设备就是一个字符一个字符的读取,例如键盘,串口等。块设备一般都有缓存,而字符设备一般没有缓存

问题一:linux下PID的取值范围?
     一般PID_MAX=0x8000(可改),因此进程号的最大值为0x7fff ,即32767。进程号0-299保留给daemon进程。现在的内核好像没有这个限制了,《linux内核设计与实现》上说为了与老版本的unix和linux兼容,pid的最大值默认是32767(short int的最大值)。如果你需要的话还可以不考虑和老版本兼容,修改/proc/sys/kernel/pid_max来提高上限用echo重新写入一个数值到这个文件即可。
      由于一般机器不可能同时跑那么多进程+线程,所以32768是肯定够用了,但是系统倾向于分配未使用过的pid给新进程,所以你会发现在正在运行的系统上,有很多低位的pid没有使用,那是因为启动的时候该pid被其它程序用过了,当然,你真有本事用到pid的最大值,系统也有办法解决,那就是从头(低位)搜索未被占用的pid分配给新进程。
问题二:linux下init 0/1/2/3/4/5/6代表的意义?
    0 - 停机(千万不能把initdefault 设置为0 )我就是用这个关机的,速度比较快! 
    1 - 单用户模式 
    2 - 多用户,没有 NFS 
    3 - 完全多用户模式(标准的运行级) 
    4 - 没有用到 
    5 - X11 (xwindow) 
    6 - 重新启动 (reboot)(千万不要把initdefault 设置为6 )
问题三:为什么有些地方说fork()调用时子进程先于父进程?
    由于内核使用-写时复制机制,fork之后父/子进程是共享页表描述符的,如果让父进程先执行,那么有很大几率父进程会修改共享页表指向的数据,那么内核此时必须给父进程分配并复制新的页表供父进程修改使用, 那么如果子进程被创建之后什么都没干就退出了,那么这个写时复制就是多余的,如果让子进程先执行,如果子进程什么都没做就退出了,那么就没有所谓的写时复制了, 避免了不必要的页面复制;另外,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。 如果父/子进程都进行了数据的修改,执行了自己的操作,那么就没有什么先后之分了!所以有些地方也说父/子进程的执行顺序是不确定的!也是有一定道理的。
问题四: exec与system的区别?
  (1) exec是直接用新的进程去代替原来的程序运行,运行完毕之后 不回到原先的程序中去
  (2) system是调用shell执行你的命令, system=fork+exec+waitpid,执行完毕之后,回到原先的程序中去。继续执行下面的
部分。
  总之,如果你用exec调用,首先应该fork一个新的进程,然后exec. 而system不需要你fork新进程,已经封装好了。
问题五:exec替换的内容?
     系统调用exec是以新的进程 (可执行的二进制文件或shell脚本文件)去代替原来的进程,仅仅进程的PID保持不变。因此,可以这样认为,exec系统调用虽然没有创建新的
进程, 但是替换了原来进程上下文的所有内容(原进程的代码段,数据段,堆栈段),确实有点儿“三十六计”中的“金蝉脱壳”的意味。看上去还是旧的躯壳,却已经注入了新的灵魂。 调用成功函数不会返回,只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。

问题六:fork()之后子进程到底复制了父进程什么?
     关于fork函数中的内存复制和共享“子进程复制父进程的数据空间(数据段)、栈和堆,父、子进程共享正文段。”也就是说,对于程序中的数据,子进程要复制一份,但是对于指令,子进程并不复制而是和父进程共享,除非执行exec函数,另起炉灶。两者的虚拟空间不同,但其对应的物理空间是同一个。
     写时复制技术(重要的事情说两遍):内核只为新生成的子进程创建虚拟空间结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。但是vfork()创建的线程,就是内核连子进程的虚拟地址空间结构也不创建了,直接共享了父进程的虚拟空间。

资料参考:

http://blog.csdn.net/xy010902100449/article/details/44851453

http://www.cnblogs.com/blankqdb/archive/2012/08/23/2652386.html

http://blog.chinaunix.net/uid-24774106-id-3361500.html
http://www.linuxidc.com/Linux/2015-03/114888.htm

http://blog.csdn.net/lollipop_jin/article/details/8774057

在此感谢博主的分享!

你可能感兴趣的:(pid,fork,init,COW)