根据题意,有上面的程序(进程)关系。
查阅资料,得知共享内存相关的函数为shmget(),shmat()等,在实验提供的linux源码文件夹下,/ipc保存着一些和进程密切相关的函数,共享内存函数的一些头文件则在/include/linux/文件夹下。
我们可以先去了解一些这些共享内存相关的函数以及其他进程通信的知识再进行下一步的实验。
在/linux-2.6.10/include/linux/shm.h 中我们可以找到源码中一块共享内存的数据结构。
其中:
#ifdef __KERNEL__
struct shmid_kernel /* private to the kernel */
{
struct kern_ipc_perm shm_perm;
struct file * shm_file;//共享内存文件
int id;
unsigned long shm_nattch;//连接到这块共享内存的进程数目
unsigned long shm_segsz;//内存大小
time_t shm_atim;
time_t shm_dtim;
time_t shm_ctim;
pid_t shm_cprid;//创建者的pid
pid_t shm_lprid;//最后操作者的pid
struct user_struct *mlock_user;
};
在/linux-2.6.10/shm.c中我们可以找到shmget()函数。
共享内存的使用主要有以下几个步骤
创建
shmget的三个参数,key为共享内存对象的键值,为0时会创建新的共享内存对象;size表示要创建的共享内存的大小;shmflg为共享内存标识符。该函数返回共享内存地址的引用标识符。
关联
创建了共享内存后,还需要将共享内存区域映射到进程的虚拟地址空间,然后才能使用这块共享内存。和这个关联操作相关的函数为shmat()。该函数的逻辑在/linux-2.6.10/shm.c中的do_shmat()函数。
long do_shmat(int shmid, char __user *shmaddr, int shmflg, ulong *raddr)
其中:
参数shmid是shmget返回的共享内存对象的引用标识符;参数shmaddr用来指定共享内存在进程虚拟地址空间中对应的虚拟地址;shmflg是映射标志。该函数返回共享内存在进程中的虚拟地址。
3.分离
当进程不再需要使用共享内存时,需要将它与共享内存分离,只有当最后一个使用该共享内存的进程与这块共享内存分离后,这块共享内存才会被释放。完成这个分离动作的函数是shmdt()。在/linux-2.6.10/shm.c中可以找到该函数。
函数原型为:
asmlinkage long sys_shmdt(char __user *shmaddr)
其中:
传入的参数为共享内存的虚拟地址,然后找到进程的所有内存结构中和这个地址对应的一个,然后调用do_munmap()来进行分离。
创建,关联,分离,这是使用共享内存的基本步骤,在对此有了大概的认识后,我们开始下一步的工作。
一个程序(进程)从客户端读入按键信息,一次将一行按键信息保存到一个共享存储的缓冲区内并等待读取进程将数据读走,不断重复上面的操作;
int main(int argc,char *argv[]){
//create the share memory by shmget()
int shm_id = shmget(IPC_PRIVATE,BUFFERSIZE,0666);
if(shm_id < 0){
perror("shmid error\n");
exit(1);
}//if
//get the virtual address of this share memory
char *shm_buf;
if((shm_buf = shmat(shm_id,0,0)) < (char*)0){
perror("shmbuffer error!\n");
exit(1);
}
//message input
char message[128];
//create the semaphore
sem_t *mutex = sem_open("mutex",O_CREAT,0666,1);
sem_t *full = sem_open("full",O_CREAT,0666,0);
sem_t *empty = sem_open("empty",O_CREAT,0666,1);
char c;
int i;
printf("shmid = %d\n",shm_id);
while(1){
i = -1;
while((c = getchar()) != '\n'){
message[++i] = c;
}//while
message[++i] = '\0';
//wait the semaphore
sem_wait(empty);
sem_wait(mutex);
//send the message to shm_buf
sprintf(shm_buf,message);
//semaphore +1
sem_post(mutex);
sem_post(full);
}//while
return 0;
}
创建共享内存和关联的工作,使用之前介绍的函数即可。这里需要注意的是,创建的共享内存的权限最好设置为666(4(可读)+2(可写) = 6),否者读进程可能无法读取该内存区域,而在输出时出现段错误。
我们把这个问题看成一个生产-消费者问题,于是可以按照书中给的方法,设置三个信号量,分别为:mutex,full,empty;
当信号量不存在时需要使用信号量时,使用如下的sem_open函数来创建一个信号量:
sem_t sem_open(const char *name,int oflag,/*mode_t mode,unsigned int value/);
第一个参数是这个有名信号量的名字,第二个参数选择创建或者打开一个信号量,第三个参数为信号量权限设置,最后一个参数为信号量的初始值。
sem_t *mutex = sem_open("mutex",O_CREAT,0666,1);
sem_t *full = sem_open("full",O_CREAT,0666,0);
sem_t *empty = sem_open("empty",O_CREAT,0666,1);
我们将三个信号量的权限都设为666,再将mutex和empty的初始值设置为1,full的初始值设置为0。
这里的临界资源为共享内存,所以只需要将对共享内存的操作,写入(读出)的代码放在临界区即可。
sem_wait相当于wait,而sem_post相当于signal,这里同样需要按照课本给出的“先私后公”的原则,以免出现死锁。于是我们有:
sem_wait(empty);
sem_wait(mutex);
//临界区
sem_post(mutex);
sem_post(full);
这便是写进程的大致情况,下面将介绍读进程。
另一个程序(进程)生成两个进程/线程,用于显示缓冲区内的信息,这两个线程并发读取缓冲区信息后将缓冲区清空(一个线程的两次显示操作之间可以加入适当的时延以便于观察)。
我们可以按照之前实验的方法,使用fork()来创建一个子进程,然父子进程并发执行,互斥地读取共享内存中的内容。
int main(int argc,char *argv[]){
if(argc != 2){
printf("input the shmid!\n");
exit(1);
}//if
//get the shmid from argv
int shm_id = atol(argv[1]);
char *shm_buf;
//get the share memory address
if((shm_buf = shmat(shm_id,0,0)) < (char*)0){
perror("shm_buf error\n");
exit(1);
}//if
sem_t *mutex = sem_open("mutex",1);
sem_t *full = sem_open("full",0);
sem_t *empty = sem_open("empty",1);
int pid = fork();
if(pid < 0){
perror("pid error\n");
}//if
else if(pid == 0){
while(1){
//wait the semaphore
sem_wait(full);
sem_wait(mutex);
printf("son pid %d receive message:%s\n",getpid(),shm_buf);
//clear the share memory
strcpy(shm_buf,"");
//semaphore +1
sem_post(mutex);
sem_post(empty);
}//while
}//else if
else{
while(1){
//wait the semaphore
sem_wait(full);
sem_wait(mutex);
printf("parent pid %d receive message:%s\n",getpid(),shm_buf);
//clear the share memory
strcpy(shm_buf,"");
//semaphore +1
sem_post(mutex);
sem_post(empty);
}//while
}//else
return 0;
}
之前说过,在使用共享内存前,要做的工作为:创建,关联,而创建工作在write进程中已经做了,这里我们只需要关联上已经创建好的共享内存即可。我们通过从程序的参数中读入已经创建好的共享内存编号(让write进程输出创建好的共享内存编号,方便我们手动输入到read进程)。
我们此时再打开信号量时只需要按照如下的方式即可:
sem_t *mutex = sem_open("mutex",1);
sem_t *full = sem_open("full",0);
sem_t *empty = sem_open("empty",1);
因为这几个有名信号量以及在write进程中创建了。
将读取共享内存和清除共享内存内容的操作放在临界区,这里父子进程的临界区操作都是一样的。
在两个独立的终端窗口上分别运行上述两个程序,展示其同步与通信功能,要求一次只有一个任务在操作缓冲区。
编译执行上面的两个程序,需要注意的是,使用gcc编译时,需要加上-lpthread 选项,否则无法链接,编译出错。
先运行write得到创建的共享内存号,
接着运行read,它带有一个参数,为我们创建的共享内存的编号。
测试结果
可以看到,进程之间正常通信,父子进程可以互斥地读取共享内存的内容。
我们可以使用ipcs -m命令来查看共享内存的使用情况,比如我们想查看我们上面实验中使用的编号为11763759的共享内存的情况,
返回的结果的第一列为key值,第二列就是共享内存编号,第三列是所有者,第四列为权限,第五列为大小,第六列为与这个共享内存关联的进程数,最后一列是共享内存的状态,dest表示已删除。
可以看到11763759的权限是666,大小是我们规定的4096,和它关联的三个进程为write,read父进程,read子进程。
我们还可以看一下,我们创建的有名信号量:
位置在/dev/shm/
可以看到,我们创建的三个信号量,mutex,empty和full。
需要注意的是,不论是共享内存还是有名信号量,我们在使用完成后都应该释放掉,避免不必要的麻烦。
通过本次实验,自己对于信号量机制和进程间的同步与通信有了更好的认识。学习使用了与共享内存相关的几个函数以及和信号量相关的几个函数的使用,并且对课本中的经典进程同步问题——生产消费者问题有了更深的认识和理解。
http://blog.csdn.net/u013580497/article/details/49883723
实验源码:链接: https://pan.baidu.com/s/1o7XLsx0 密码: tjej