#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;
}
运行结果:
但是为什么父/子进程指向的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;
}
运行结果:
运行的结果乍一看可能不太懂,让我慢慢来解释,*号代表每个进程都会打印出一个*,+号代表的是创建的进程数。因为这里的进程不是共享一个变量,所以我想用count计数器来计数,却办不到,因为每个计数器count都是从1开始的,最后还得加起来,看起来更乱,所以我就用*和+来表示更直观。这也证明了一个问题,那就是fork出的函数确实是资源空间的复制而不是指针的复制(vfork)!!很好!
因为fork是父进程创建一个子进程,所以为什么*会比+多,是因为进程有重复的,但是退出是一定的且只会退出一次,所以+刚好可以代表创建了几个进程。仔细想一想就会明白了。。。
其实其进程创建的过程是这样的:
数字代表进程号,这就是具体产生的过程。
下面我们可以通过查看其PID看一下创建的进程:
从图中可以看出:共产生了进程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
在此感谢博主的分享!