全部博文(2759)
分类: LINUX
2014-02-21 13:06:08
原文地址:共享内存的常用函数原理剖析 作者:chaohona
象所有的 System V IPC对象一样,对于共享内存对象的获取是由key控制。内存共享之后,对进程如何使用这块内存就不再做检查。它们必须依赖于其它机制,比如System V的信号灯来同步对于共享内存区域的访问(信号灯如何控制对临界代码的访问另起一篇说话)。
每一个新创建的共享内存对象都用一个shmid_kernel数据结构来表达。系统中所有的shmid_kernel数据结构都保存在shm_segs向量表中,该向量表的每一个元素都是一个指向shmid_kernel数据结构的指针。
shm_segs向量表的定义如下:
struct shmid_kernel *shm_segs[SHMMNI];
SHMMNI为128,表示系统中最多可以有128个共享内存对象。
数据结构shmid_kernel的定义如下:
struct shmid_kernel
{
struct shmid_ds u;
unsigned long shm_npages;
unsigned long *shm_pages;
struct vm_area_struct *attaches;
};
其中:
shm_pages代表该共享内存对象的所占据的内存页面数组,数组里面的每个元素当然是每个内存页面的起始地址.
shm_npages则是该共享内存对象占用内存页面的个数,以页为单位。这个数量当然涵盖了申请空间的最小整数倍.
(A new shared memory segment, with size equal to the value of size rounded up to a multiple of PAGE_SIZE)
shmid_ds是一个数据结构,它描述了这个共享内存区的认证信息,字节大小,最后一次粘附时间、分离时间、改变时间,创建该共享区域的进程,最后一次对它操作的进程,当前有多少个进程在使用它等信息。
其定义如下:
struct shmid_ds {
struct ipc_perm shm_perm;
int shm_segsz;
__kernel_time_t shm_atime;
__kernel_time_t shm_dtime;
__kernel_time_t shm_ctime;
__kernel_ipc_pid_t shm_cpid;
__kernel_ipc_pid_t shm_lpid;
unsigned short shm_nattch;
unsigned short shm_unused;
void *shm_unused2;
void *shm_unused3;
};
attaches描述被共享的物理内存对象所映射的各进程的虚拟内存区域。每一个希望共享这块内存的进程都必须通过系统调用将其关联(attach)到它的虚拟内存中。这一过程将为该进程创建了一个新的描述这块共享内存的vm_area_struct数据结构。创建时可以指定共享内存在它的虚拟地址空间的位置,也可以让Linux自己为它选择一块足够的空闲区域。
这个新的vm_area_struct结构是维系共享内存和使用它的进程之间的关系的,所以除了要关联进程信息外,还要指明这个共享内存数据结构shmid_kernel所在位置; 另外,便于管理这些经常变化的vm_area_struct,所以采取了链表形式组织这些数据结构,链表由attaches指向,同时 vm_area_struct数据结构中专门提供了两个指针:vm_next_shared和 vm_prev_shared,用于连接该共享区域在使用它的各进程中所对应的vm_area_struct数据结构。
Linux为共享内存提供了四种操作。
1. 共享内存对象的创建或获得。与其它两种IPC机制一样,进程在使用共享内存区域以前,必须通过系统调用sys_ipc (call值为SHMGET)创建一个键值为key的共享内存对象,或获得已经存在的键值为key的某共享内存对象的引用标识符。以后对共享内存对象的访问都通过该引用标识符进行。对共享内存对象的创建或获得由函数sys_shmget完成,其定义如下:
int sys_shmget (key_t key, int size, int shmflg)
这里key是表示该共享内存对象的键值,size是该共享内存区域的大小(以字节为单位),shmflg是标志(对该共享内存对象的特殊要求)。
它所做的工作如下:
1) 如果key == IPC_PRIVATE,则总是会创建一个新的共享内存对象。
但是 (The name choice IPC_PRIVATE was perhaps unfortunate, IPC_NEW would more clearly show its function)
* 算出size要占用的页数,检查其合法性。
* 申请一块内存用于建立shmid_kernel数据结构,注意这里申请的内存区域大小不包括真正的共享内存区,实际上,要等到第一个进程试图访问它的时候才真正创建共享内存区。
* 根据该共享内存区所占用的页数,为其申请一块空间用于建立页表(每页4个字节),将页表清0。
* 搜索向量表shm_segs,为新创建的共享内存对象找一个空位置。
* 填写shmid_kernel数据结构,将其加入到向量表shm_segs中为其找到的空位置。
* 返回该共享内存对象的引用标识符。
2) 在向量表shm_segs中查找键值为key的共享内存对象,结果有三:
* 如果没有找到,而且在操作标志shmflg中没有指明要创建新共享内存,则错误返回,否则创建一个新的共享内存对象。
* 如果找到了,但该次操作要求必须创建一个键值为key的新对象,那么错误返回。
* 否则,合法性、认证检查,如有错,则错误返回;否则,返回该内存对象的引用标识符。
共享内存对象的创建者可以控制对于这块内存的访问权限和它的key是公开还是私有。如果有足够的权限,它也可以把共享内存锁定在物理内存中。
参见include/linux/shm.h
2. 关联。在创建或获得某个共享内存区域的引用标识符后,还必须将共享内存区域映射(粘附)到进程的虚拟地址空间,然后才能使用该共享内存区域。系统调用 sys_ipc(call值为SHMAT)用于共享内存区到进程虚拟地址空间的映射,而真正完成粘附动作的是函数sys_shmat,
其定义如下:
#include
#include
void *shmat(int shmid, const void *shmaddr, int shmflg);
其中:
shmid是shmget返回的共享内存对象的引用标识符;
shmaddr用来指定该共享内存区域在进程的虚拟地址空间对应的虚拟地址;
shmflg是映射标志;
返回的是在进程中的虚拟地址
该函数所做的工作如下:
1) 根据shmid找到共享内存对象。
2) 如果shmaddr为0,即用户没有指定该共享内存区域在它的虚拟空间中的位置,则由系统在进程的虚拟地址空间中为其找一块区域(从1G开始);否则,就用shmaddr作为映射的虚拟地址。
(If shmaddr is NULL, the system chooses a suitable (unused) address a他 which to attach the segment)
3) 检查虚拟地址的合法性(不能超过进程的最大虚拟空间大小—3G,不能太接近堆栈栈顶)。
4) 认证检查。
5) 申请一块内存用于建立数据结构vm_area_struct,填写该结构。
6) 检查该内存区域,将其加入到进程的mm结构和该共享内存对象的vm_area_struct队列中。
共享内存的粘附只是创建一个vm_area_struct数据结构,并将其加入到相应的队列中,此时并没有创建真正的共享内存页。
当进程第一次访问共享虚拟内存的某页时,因为所有的共享内存页还都没有分配,所以会发生一个page fault异常。当Linux处理这个page fault的时候,它找到发生异常的虚拟地址所在的vm_area_struct数据结构。在该数据结构中包含有这类共享虚拟内存的一组处理程序,其中的 nopage操作用来处理虚拟页对应的物理页不存在的情况。对共享内存,该操作是shm_nopage(定义在ipc/shm.c中)。该操作在描述这个共享内存的shmid_kernel数据结构的页表shm_pages中查找发生page fault异常的虚拟地址所对应的页表条目,看共享页是否存在(页表条目为0,表示共享页是第一次使用)。如果不存在,它就分配一个物理页,并为它创建一个页表条目。这个条目不但进入当前进程的页表,同时也存到shmid_kernel数据结构的页表shm_pages中。
当下一个进程试图访问这块内存并得到一个page fault的时候,经过同样的路径,也会走到函数shm_nopage。此时,该函数查看shmid_kernel数据结构的页表shm_pages时,发现共享页已经存在,它只需把这里的页表项填到进程页表的相应位置即可,而不需要重新创建物理页。所以,是第一个访问共享内存页的进程使得这一页被创建,而随后访问它的其它进程仅把此页加到它们的虚拟地址空间。
3. 分离。当进程不再需要共享虚拟内存的时候,它们与之分离(detach)。只要仍旧有其它进程在使用这块内存,这种分离就只会影响当前的进程,而不会影响其它进程。当前进程的vm_area_struct数据结构被从shmid_ds中删除,并被释放。当前进程的页表也被更新,共享内存对应的虚拟内存页被标记为无效。当共享这块内存的最后一个进程与之分离时,共享内存页被释放,同时,这块共享内存的shmid_kernel数据结构也被释放。
系统调用sys_ipc (call值为SHMDT) 用于共享内存区与进程虚拟地址空间的分离,而真正完成分离动作的是函数
sys_shmdt,其定义如下:
int sys_shmdt (char *shmaddr)
其中shmaddr是进程要分离的共享页的开始虚拟地址。
该函数搜索进程的内存结构中的所有vm_area_struct数据结构,找到地址shmaddr对应的一个,调用函数do_munmap将其释放。
在函数do_munmap中,将要释放的vm_area_struct数据结构从进程的虚拟内存中摘下,清除它在进程页表中对应的页表项(可能占多个页表项).
如果共享的虚拟内存没有被锁定在物理内存中,分离会更加复杂。因为在这种情况下,共享内存的页可能在系统大量使用内存的时候被交换到系统的交换磁盘。为了避免这种情况,可以通过下面的控制操作,将某共享内存页锁定在物理内存不允许向外交换。共享内存的换出和换入,已在第3章中讨论。
4. 控制。Linux在共享内存上实现的第四种操作是共享内存的控制(call值为SHMCTL的sys_ipc调用),它由函数sys_shmctl实现。控制操作包括获得共享内存对象的状态,设置共享内存对象的参数(如uid、gid、mode、ctime等),将共享内存对象在内存中锁定和释放(在对象的mode上增加或去除SHM_LOCKED标志),释放共享内存对象资源等。
共享内存提供了一种快速灵活的机制,它允许进程之间直接共享大量的数据,而无须使用拷贝或系统调用。共享内存的主要局限性是它不能提供同步,如果两个进程企图修改相同的共享内存区域,由于内核不能串行化这些动作,因此写的数据可能任意地互相混合。所以使用共享内存的进程必须设计它们自己的同步协议,如用信号灯等。
以下是使用共享内存机制进行进程间通信的基本操作:
需要包含的头文件:
#include
#include
#include
1.创建共享内存:
int shmget(key_t key,int size,int shmflg);
参数说明:
key:用来表示新建或者已经存在的共享内存去的关键字。
size:创建共享内存的大小。
shmflg:可以指定的特殊标志。IPC_CREATE,IPC_EXCL以及低九位的权限。
eg:
int shmid;
shmid=shmget(IPC_PRIVATE,4096,IPC_CREATE|IPC_EXCL|0660);
if(shmid==-1)
perror("shmget()");
2.连接共享内存
char *shmat(int shmid,char *shmaddr,int shmflg);
参数说明
shmid:共享内存的关键字
shmaddr:指定共享内存出现在进程内存地址的什么位置,通常我们让内核自己决定一个合适的地址位置,用的时候设为0。
shmflg:制定特殊的标志位。
eg:
int shmid;
char *shmp;
shmp=shmat(shmid,0,0);
if(shmp==(char *)(-1))
perror("shmat()\n");
3.使用共享内存
在使用共享内存是需要注意的是,为防止内存访问冲突,我们一般与信号量结合使用。
4.分离共享内存:当程序不再需要共享内后,我们需要将共享内存分离以便对其进行释放,分离共享内存的函数原形如下:
int shmdt(char *shmaddr);
5. 释放共享内存
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
Posix有名信号灯
1.posix有名信号灯函数
函数sem_open创建一个新的有名信号灯或打开一个已存在的有名信号灯。有名信号灯总是既可用于线程间的同步,又能用于进程间的同步。
1. sem_open
名称::
sem_open
功能:
创建并初始化有名信号灯
头文件:
#include
函数原形:
sem_t *sem_open(const char *name,int oflag,/*mode_t mode,unsigned int value*/);
参数:
name 信号灯的外部名字
oflag 选择创建或打开一个现有的信号灯
mode 权限位
value 信号灯初始值
返回值:
成功时返回指向信号灯的指针,出错时为SEM_FAILED
oflag参数能是0、O_CREAT(创建一个信号灯)或O_CREAT|O_EXCL(如果没有指定的信号灯就创建),如果指定了O_CREAT,那么第三个和第四个参数是需要的;其中mode参数指定权限位,value参数指定信号灯的初始值,通常用来指定共享资源的书面。该初始不能超过 SEM_VALUE_MAX,这个常值必须低于为32767。二值信号灯的初始值通常为1,计数信号灯的初始值则往往大于1。
如果指定了O_CREAT(而没有指定O_EXCL),那么只有所需的信号灯尚未存在时才初始化他。所需信号灯已存在条件下指定O_CREAT不是个错误。该标志的意思仅仅是“如果所需信号灯尚未存在,那就创建并初始化他”。不过所需信号灯等已存在条件下指定O_CREAT|O_EXCL却是个错误。
sem_open返回指向sem_t信号灯的指针,该结构里记录着当前共享资源的数目。
名称::
sem_close
功能:
关闭有名信号灯
头文件:
#include
函数原形:
int sem_close(sem_t *sem);
参数:
sem 指向信号灯的指针
返回值:
若成功则返回0,否则返回-1。
一个进程终止时,内核还对其上仍然打开着的所有有名信号灯自动执行这样的信号灯关闭操作。不论该进程是自愿终止的还是非自愿终止的,这种自动关闭都会发生。
但应注意的是关闭一个信号灯并没有将他从系统中删除。这就是说,Posix有名信号灯至少是随内核持续的:即使当前没有进程打开着某个信号灯,他的值仍然保持。
3. sem_unlink
名称::
sem_unlink
功能:
从系统中删除信号灯
头文件:
#include
函数原形:
int sem_unlink(count char *name);
参数:
name 信号灯的外部名字
返回值:
若成功则返回0,否则返回-1。
有名信号灯使用sem_unlink从系统中删除。
每个信号灯有一个引用计数器记录当前的打开次数,sem_unlink必须等待这个数为0时才能把name所指的信号灯从文件系统中删除。也就是要等待最后一个sem_close发生。
4. sem_getvalue
名称::
sem_getvalue
功能:
测试信号灯
头文件:
#include
函数原形:
int sem_getvalue(sem_t *sem,int *valp);
参数:
sem 指向信号灯的指针
返回值:
若成功则返回0,否则返回-1。
sem_getvalue在由valp指向的正数中返回所指定信号灯的当前值。如果该信号灯当前已上锁,那么返回值或为0,或为某个负数,其绝对值就是等待该信号灯解锁的线程数。
5. sem_wait/sem_trywait
名称::
sem_wait/sem_trywait
功能:
等待共享资源
头文件:
#include
函数原形:
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
参数:
sem 指向信号灯的指针
返回值:
若成功则返回0,否则返回-1。
我们能用sem_wait来申请共享资源,sem_wait函数能测试所指定信号灯的值,如果该值大于0,那就将他减1并即时返回。我们就能使用申请来的共享资源了。如果该值等于0,调用线程就被进入睡眠状态,直到该值变为大于0,这时再将他减1,函数随后返回。sem_wait操作必须是原子的。 sem_wait和sem_trywait的差别是:当所指定信号灯的值已是0时,后者并不将调用线程投入睡眠。相反,他返回一个EAGAIN错误。
下面的程式我们先不去运行,稍后再运行。
6. sem_post
名称::
sem_post
功能:
挂出共享资源
头文件:
#include
函数原形:
int sem_post(sem_t *sem);
int sem_getvalue(sem_t *sem,int *valp);
参数:
sem 指向信号灯的指针
返回值:
若成功则返回0,否则返回-1。
当一个线程使用完某个信号灯时,他应该调用sem_post来告诉系统申请的资源已用完。本函数和sem_wait函数的功能正好相反,他把所指定的信号灯的值加1,然后唤醒正在等待该信号灯值变为正数的任意线程。
下面的程式我们先不去运行,稍后再运行。
我们要注意以下几点:
1.Posix有名信号灯的值是随内核持续的。也就是说,一个进程创建了一个信号灯,这个进程结束后,这个信号灯还存在,并且信号灯的值也不会改动。
下面我们利用上面的几个程式来证实这点
#./semopen test
#./semgetvalue test
value=1 信号的值仍然是1
2。当持有某个信号灯锁的进程没有释放他就终止时,内核并不给该信号灯解锁。
#./semopen test
#./semwait test&
pid 1834 has semaphore,value=0
#./semgetvalue test
value=0 信号量的值变为0了
3.posix有名信号灯应用于多线程
程式用循环的方法建立5个线程,然后让他们调用同一个线程处理函数thread_function,在函数里我们利用信号量来限制访问共享资源的线程数。共享资源我们用print函数来代表,在真正编程中他有能是个终端设备(如打印机)或是一段有实际意义的代码。
运行结果为:
#gcc sem1.c -o sem1 -lrt -lpthread
#.I get it,my tid is 1082330304
Now the value have 2
Iget it,my pid is 1894
Now the value have 1
Iget it,my pid is 1895
Now the value have 0
I’m finished,my pid is 1893
I’m finished,my pid is 1894
I’m finished,my pid is 1895
I get it,my pid is 1896
Now the value have 2
I get it,mypid is 1897
Now the value have 1
I’m finished,my pid is 1896
I’m finished,my pid is 1897
4.posix有名信号灯应用于多进程
下面就是应用Posix有名信号灯的一个小程序。用它来限制访问共享代码的进程数目。
#include
#include
#include
#include
void print(pid_t);
sem_t *sem; /*定义Posix有名信号灯*/
int val; /*定义信号灯当前值*/
int main(int argc,char *argv[])
{
int n=0;
if(argc!=2)
{
printf(“please input a file name!\n”);
exit(1);
}
sem=sem_open(argv[1],O_CREAT,0644,2); /*打开一个信号灯, 初值设为2*/
while(n++循环创建5个子进程,使它们同步运行*/
{
if(fork()==0)
{
sem_wait(sem); /*申请信号灯*/
print(getpid()); /*调用共享代码段*/
sleep(1);
sem_post(sem); /*释放信号灯*/
printf(“I’m finished,my pid is %d\n”,getpid());
return 0;
}
}
wait(); /*等待所有子进程结束*/
sem_close(sem);
sem_unlink(argv[1]);
exit(0);
}
void print(pid_t pid)
{
printf(“I get it,my pid is %d\n”,pid);
sem_getvalue(sem,&val);
printf(“Now the value have %d\n”,val);
}
程序编译后运行会得到如下结果:
#./8_2 8_2.c
I get it,my tid is 1082330304
Now the value have 1
I get it,my tid is 1090718784
Now the value have 0
I finished,my pid is 1082330304
I finished,my pid is 1090718784
I get it,my tid is 1099107264
Now the value have 1
I get it,my tid is 1116841120
Now the value have 0
I finished,my pid is 1099107264
I finished,my pid is 1116841120
I get it,my tid is 1125329600
Now the value have 1
I finished,my pid is 1125329600
三、基于内存的信号灯
前面已经介绍了Posix有名信号灯。这些信号灯由一个name参数标识,它通常指代文件系统中的某个文件。然而Posix也提供基于内存的信号灯,它们由应用程序分配信号灯的内存空间,然后由系统初始化它们的值。
7.
名称::
sem_init/sem_destroy
功能:
初始化/关闭信号等
头文件:
#include
函数原形:
int sem_init(sem_t *sem,int shared,unsigned int value);
int sem_getvalue(sem_t *sem);
参数:
sem 指向信号灯的指针
shared 作用范围
value 信号灯初始值
返回值:
若成功则返回0,否则返回-1。
基于内存的信号灯是由sem_init初始化的。sem参数指向必须由应用程序分配的sem_t变量。如果shared为0,那么待初始化的信号灯是在同一进程的各个线程共享的,否则该信号灯是在进程间共享的。当shared为零时,该信号灯必须存放在即将使用它的所有进程都能访问的某种类型的共享内存中。跟sem_open一样,value参数是该信号灯的初始值。
使用完一个基于内存的信号灯后,我们调用sem_destroy关闭它。
除了sem_open和sem_close外,其它的poisx有名信号灯函数都可以用于基于内存的信号灯。
注意:posix基于内存的信号灯和posix有名信号灯有一些区别,我们必须注意到这些。
1.sem_open不需要类型与shared的参数,有名信号灯总是可以在不同进程间共享的。
2.sem_init不使用任何类似于O_CREAT标志的东西,也就是说,sem_init总是初始化信号灯的值。因此,对于一个给定的信号灯,我们必须小心保证只调用一次sem_init。
3.sem_open返回一个指向某个sem_t变量的指针,该变量由函数本身分配并初始化。但sem_init的第一个参数是一个指向某个sem_t变量的指针,该变量由调用者分配,然后由sem_init函数初始化。
4.posix有名信号灯是通过内核持续的,一个进程创建一个信号灯,另外的进程可以通过该信号灯的外部名(创建信号灯使用的文件名)来访问它。 posix基于内存的信号灯的持续性却是不定的,如果基于内存的信号灯是由单个进程内的各个线程共享的,那么该信号灯就是随进程持续的,当该进程终止时它也会消失。如果某个基于内存的信号灯是在不同进程间同步的,该信号灯必须存放在共享内存区中,这要只要该共享内存区存在,该信号灯就存在。
5.基于内存的信号灯应用于线程很麻烦(待会你会知道为什么),而有名信号灯却很方便,基于内存的信号灯比较适合应用于一个进程的多个线程。
下面是posix基于内存的信号灯实现一个进程的各个线程间的互次。
#include
#include
#include
#include
#include
#incude
void *thread_function(void *arg); /*线程入口函数*/
void print(void); /*共享资源函数*/
sem_t bin_sem; /*定义信号灯*/
int value; /*定义信号量的灯*/
int main()
{
int n=0;
pthread_t a_thread;
if((sem_init(&bin_sem,0,2))!=0) /*初始化信号灯,初始值为2*/
{
perror(“sem_init”);
exit(1);
}
while(n++循环创建5个线程*/
{
if((pthread_create(&a_thread,NULL,thread_function,NULL))!=0)
{
perror(“Thread creation failed”);
exit(1);
}
}
pthread_join(a_thread,NULL);/*等待子线程返回*/
}
void *thread_function(void *arg)
{
sem_wait(&bin_sem); /*等待信号灯*/
print();
sleep(1);
sem_post(&bin_sem); /*挂出信号灯*/
printf(“I finished,my pid is %d\n”,pthread_self());
pthread_exit(arg);
}
void print()
{
printf(“I get it,my tid is %d\n”,pthread_self());
sem_getvalue(&bin_sem,&value); /*获取信号灯的值*/
printf(“Now the value have %d\n”,value);
}
posix基于内存的信号灯和有名信号灯基本是一样的,上面的几点区别就可以了。
下面是运行结果:
#gcc –lpthread –o seminitthread seminitthread.c
#./seminitthread
I get it,my tid is 1082330304
Now the value have 1
I get it,my tid is 1090718784
Now the value have 0
I finished,my pid is 1082330304
I finished,my pid is 1090718784
I get it,my tid is 1099107264
Now the value have 1
I get it,my tid is 1116841120
Now the value have 0
I finished,my pid is 1099107264
I finished,my pid is 1116841120
I get it,my tid is 1125329600
Now the value have 1
I finished,my pid is 1125329600
下面的程序并不能得到我们想要的结果。
#include
#include
#include
#include
void print(pid_t);
sem_t *sem; /*定义Posix有名信号灯*/
int val; /*定义信号灯当前值*/
int main(int argc,char *argv[])
{
int n=0;
sem=sem_open(argv[1],O_CREAT,0644,3); /*打开一个信号灯*/
sem_getvalue(sem,&val); /*查看信号灯的值*/
printf(“The value have %d\n”,val);
while(n++循环创建5个子进程,使它们同步运行*/
{
if(fork()==0)
{
sem_wait(sem); /*申请信号灯*/
print(getpid()); /*调用共享代码段*/
sleep(1);
sem_post(sem); /*释放信号灯*/
printf(“I’m finished,my pid is %d\n”,getpid());
return 0;
}
wait(); /*等待所有子进程结束*/
return 0;
}
void print(pid_t pid)
{
printf(“I get it,my pid is %d\n”,pid);
sem_getvalue(sem,&val);
printf(“Now the value have %d\n”,val);
}
下面是运行结果:
#cc –lpthread –o sem sem.c
#./sem
The value have 3
I get it,my pid is 2236
Now the value have 2
I get it,my pid is 2237
Now the value have 2
I get it,my pid is 2238
Now the value have 2
I get it,my pid is 2239
Now the value have 2
Iget it,my pid is 2240
Now the value have 2
I’m finished,my pid is 2236
I’m finished,my pid is 2237
I’m finished,my pid is 2238
I’m finished,my pid is 2239
I’m finished,my pid is 2240
问题在于sem信号灯不在共享内存区中。fork出来的子进程通常不共享父进程的内存空间。子进程是在父进程内存空间的拷贝上启动的,它跟共享内存不是一回事。