版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
进程控制
每个进程都有一个非负整数表示的唯一进程ID
虽然唯一,不过可以复用,但不是立刻复用,而是使用延迟算法,防止将新进程误认为是使用同一ID的某个已经终止的先前进程.
特殊进程:
ID为0的是调度进程,该进程是内核的一部分,不执行任何磁盘上的程序
ID为1的是Init进程,init通常读取与系统有关的初始化文件(/etc/rc*文件、/etc/inittab文件、/etc/init.d/中的文件)
ID为2的是页守护进程,负责支持虚拟存储器系统的分页操作
除了进程ID,每个进程还有一些其他标识符:
#include <unistd.h>
pid_t getpid(void);//返回调用进程的进程ID
pid_t getppid(void);//调用进程的父进程ID
uid_t getuid(void);//调用进程的实际用户ID
uid_geteuid(void);//调用进程的有效用户ID
gid_t getgid(void);//调用进程的实际组ID
gid_t getegid(void);//调用进程的有效组ID
fork调用
#include <unistd.h>
pid_t fork(void);
//子进程返回0
//父进程返回子进程ID
//出错返回-1
fork
函数被调用一次将返回两次,在子进程中返回0,在父进程中返回子进程的ID。
子进程获得父进程的数据空间、堆、栈副本
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <sys/time.h>
#include <malloc.h>
int globvar=6;//全局变量
char buf[]="hello world\r\n";
int main(void )
{
int var;//栈上变量
pid_t pid;
var = 88;
int *ptr=(int *)malloc(sizeof(int));
*ptr=2;
if(write(STDOUT_FILENO,buf,sizeof(buf)-1)!=sizeof(buf)-1)
{
printf("write error\r\n");
return -1;
}
printf("before fork\r\n");
if((pid=fork())<0)
{
printf("fork error");
return -1;
}
else if(pid==0)//child
{
++*ptr;
++var;
++globvar;
}
else//parent
{
sleep(2);
}
printf("pid = %ld, globvar = %d, &var = %ld , var = %d , *ptr = %d , ptr=%ld\r\n",(long)getpid(),globvar,(long)&var, var ,*ptr,(long)ptr);
free(ptr);
return 0;
}
直接执行:
./fork
打印结果:
hello world
before fork
pid = 16722, globvar = 7, &var = 140722924809824 , var = 89 , *ptr = 3 , ptr=22728720
pid = 16721, globvar = 6, &var = 140722924809824 , var = 88 , *ptr = 2 , ptr=22728720
我们看到地址都是一样的,但是值不一样,说明子进程中发生了拷贝,但是为什么地址一样呢?
这里就涉及到物理地址和逻辑地址(或称虚拟地址)的概念。
操作系统讲逻辑地址转化成物理地址的过程叫做地址重定位。
分为:
静态重定位–在程序装入主存时已经完成了逻辑地址到物理地址和变换,在程序执行期间不会再发生改变。
动态重定位–程序执行期间完成,其实现依赖于硬件地址变换机构,如基址寄存器。
逻辑地址:
在计算机体系结构中是指应用程序角度看到的内存单元(memory cell)、存储单元(storage element)、网络主机(network host)的地址。
逻辑地址往往不同于物理地址(physical address),通过地址翻译器(address translator)或映射函数可以把逻辑地址转化为物理地址。
物理地址:
物理地址(英语:physical address),也叫实地址(real address)、二进制地址(binary address),
它是在地址总线上,以电子形式存在的,使得数据总线可以访问主存的某个特定存储单元的内存地址。
在和虚拟内存的计算机中,物理地址这个术语多用于区分虚拟地址。尤其是在使用内存管理单元(MMU)转换内存地址的计算机中,
虚拟和物理地址分别指在经MMU转换之前和之后的地址。
网上看到一篇很好的介绍物理地址、逻辑地址的博客:
http://www.cppblog.com/fwxjj/archive/2009/05/27/85897.html
了解了物理地址和逻辑地址,再看上述问题:
在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,
但其对应的物理空间是同一个。
当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,
如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。
而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。
fork时子进程获得父进程数据空间、堆和栈的复制,所以变量的地址(当然是虚拟地址)也是一样的。
每个进程都有自己的虚拟地址空间,不同进程的相同的虚拟地址显然可以对应不同的物理地址。因此地址相同(虚拟地址)而值不同没什么奇怪。
具体过程是这样的:
fork子进程完全复制父进程的栈空间,也复制了页表,但没有复制物理页面,所以这时虚拟地址相同,物理地址也相同,
但是会把父子共享的页面标记为“只读”类似mmap的private的方式),如果父子进程一直对这个页面是同一个页面
,直到其中任何一个进程要对共享的页面“写操作”,这时内核会复制一个物理页面给这个进程使用,同时修改页表。
而把原来的只读页面标记为“可写”,留给另外一个进程使用这就是所谓的“写时复制”
参考:http://www.cnblogs.com/zhangchaoyang/articles/2317420.html
上述代码如果执行:
./fork > see.txt
则打开see.txt文件,输出为:
hello world
before fork
pid = 14001, globvar = 7, &var = 140725591119472 , var = 89 , *ptr = 3
before fork
pid = 14000, globvar = 6, &var = 140725591119472 , var = 88 , *ptr = 2
多打印了一个before fork
这是什么原因?
首先,stdin和stdout都是行缓冲,也就是遇到\n
将flush缓冲区,因此在之前直接执行./fork
时只打印一个before fork
因为缓冲区刷新了。
但是当重定向文件时,变成了标准输出变成全缓冲,因此,子进程就复制了缓冲区。
用一句话解释:
面向终端的缓冲时行缓冲,当并不指向交互式设备时,他们是全缓冲
因此,子进程复制了父进程的缓冲区
参考:http://blog.csdn.net/zhangxiao93/article/details/70666125