还记得在上篇文章中我们说到的N个子进程独自访问自己的临界区吗,那么今天要说的共享内存就可以实现多个进程都去访问一块内存! 如果你对我们前面所说的几种IPC方式都了解,那么关于共享内存的基本知识你就有似曾相识的感觉。我们快速入门吧 !
1.如何创建一块共享内存?
其实列出函数原型,你应该会知道每个参数的含义。
int shmget(key_t key,size_t size,int shmflg);
这里要说明的是size的大小,如果创建一块新的共享内存区,那么size必须大于0;如果size为0,那此时shmget函数的功能就是打开一个已存在的共享内存区(当然,key代表的内存区是否存在那是二话)。size为负,那么调用出错,返回-1。另外shmflg的取值以及用法都与信号量相同,可参考前面的文章。
当shmget调用成功后,返回整形id。
2.共享内存创建好了,进程如何使用?
我们创建好了内存块,但是创建进程此时和这块内存还没联系。所以,我们应该使用shmat函数将其附加到进程的地址空间(具体位于进程的堆栈段)。
void* shmat(int shmid,const void* shmaddr,int shmflg);
一般我们将shmaddr置为NULL,因为此时内存会自动分配一段何时的内存块。并且最终shmat函数将返回这个内存块的首地址。也许你对此处的void类型比较不解,其实void* shmaddr这样的定义方式其实是为了灵活使用:首先shmaddr是一个指针类型(内存地址),但其具体是指向何种类型的指针,在此并不具体说明。因为当你要使用这个指针时,可以用任何类型强制转换。
与此函数对应的是shmdt函数,它会让调用进程与shmaddr指向的共享内存脱离。
3.对共享内存的控制?
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
cmd可取值:IPC_RMID,IPC_SET,IPC_STAT分别对应:删除共享内存区,设置共享内存区的shmid_ds结构,获取shmid_ds结构。这里不再赘述具体使用方法,可参考man手册。
介绍完这些,我们的重点就是要学会应用共享内存。《linuxC编程实战》一书中的例10-17非常经典,可以模拟进程之间的同步。该例还特别的定义了一个头文件shm.h,此头文件中加入了许多对信号量以及共享内存的操作函数。现在我们具体来分析,哪些语句实现进程的互斥,哪些语句实现的是进程的同步。
首先,不管是读进程,还是写进程,PV操作之间的代码都属于临界区。临界区的代码都是对共享内存去进行操作,所以应该加上PV操作,避免读写进程交互对共享内存进行访问。所以读写程序中的PV操作实现的是进程的互斥。
我们还可以发现,读写进程代码中都有waitsem(semid,0)语句。以写进程为例,每次写内存时,都要查看信号量(semid信号量集中的0号信号量)是否为1。如果为1,说明此时没有读进程访问共享内存;否则,说明有读进程在访问,此时写进程本应该阻塞,但是这里我们用sleep(1)来模拟进程的阻塞,而且不断的去测试信号量是否为1。
如果写进程成功进入临界区,那么它将写数据至共享内存。可能你对于strcpy(shmaddr,wbuf);这样的语句感到困惑:怎么能将wbuf赋值给共享内存?!其实仔细想想,这没有什么不合理。shmaddr是一个指针,wbuf是一个字符串数组的首字符的首地址,那么strcpy函数所做的就是很平常的事了。同时,我们也可以这么想:一般我们都是提前用malloc来分配内存空间,再让某个指针指向这片内存空间,而现在,上面的shmget以及shat以及完成了类似malloc的工作。
其实我们有这样的想法很自然,毕竟刚开始学习,通过实践可以从感性上认识这些。
那么很显然,waitsem(semid,0)语句实现的便是读写进程的同步问题——协调读写进程对共享内存的访问。
//写进程部分代码 while(1) { waitsem(semid,0); p(semid,0); printf("writer:"); fgets(wbuf,1024,stdin); wbuf[strlen(wbuf)-1]='\0'; strcpy(shmaddr,wbuf); my_sleep(5); v(semid,0); my_sleep(3); } //读进程部分代码 while(1) { waitsem(semid,0); p(semid,0); printf("reader:"); printf("%s\n",shmaddr); my_sleep(5); v(semid,0); my_sleep(3); }
理解了这个程序要干什么,那么我们再来想想它为什么要这么做:读写进程中为何要加入sleep函数?那我们来做几个测试吧。在测试之前,建议大家适当的修改waitsem函数和my_sleep函数,它可以让我们更好的感知当前进程的“死活”。
int waitsem(int semid,int index) { while(semctl(semid,index,GETVAL,0)==0) { sleep(1); printf("I am waiting ..\n"); } return 1; } void my_sleep(int i) { while(i--) { printf("writer is sleeping..\n");//写进程中的my_sleep做相应改动 sleep(1); } }
这样增加了一些提示语,再分别在两个终端运行读写进程,可以发现第一个my_sleep函数的作用就是故意延长读(写)进程在临界区的访问时间,以使得写(读)进程阻塞。而第二个my_sleep函数则是故意阻塞写(读)进程,让读(写)进程有时间去读共享内存。好了,既然现在掌握了信号量和共享内存,那么我们就可以试着去完成一些简单的同步互斥的题目了,继续加油吧 !