linux下进程间共享内存通信的问题

linux下进程间共享内存通信的问题
我在学习linux进程间的共享内存通信时,遇到了很多问题,就想把这些问题记下来,当做一个学习笔记。

先简单介绍一下共享内存通信:
共享内存是运行在同一台机器上的进程间通信最快的方式,因为数据不需要在不同的进程间复制。
通常由一个进程创建一块共享内存区,其余进程对这块内存区进行读写。
共享内存往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。否则,就可能出现无法意料的错误。
因为共享内存在进程结束后不会自动删除,所以你要在确保没有进程使用共享内存的时候,删除它,否则就可能造成内存泄露。

创建共享内存通信的基本步骤:
(1)首先由需要通信的任意一方进程创建一个共享内存区,并返回可供通信双方进程访问的该共享内存区的一个标识,但一定要确保两个进程得到的标识是一样的,否则,就读写的不是同一个共享内存。
(2)共享内存区非创建进程将共享内存区通过其标识绑定到本进程
(3)通信双方进程可以读写共享内存区
(4)通信结束后,双方进程脱离共享内存区
(5)任意一方通信进程删除共享内存区

我在学习老师给提供的两个进程间共享内存通信的例子时,时不时出现一些非常奇怪的错误,比如说,在写进程运行过后,运行读进程,出现没有共享内存的错误等等。
最后,我才发现了问题出在了那里,下面来一一说明:
给出老师给的两个程序(经过了我的修改):
写入程序:
/*************************************************************************
	> File Name: shmmutexwrite.c
	> Author: gwq
	> Mail: 457781132@qq.com 
	> Created Time: 2014年10月13日 星期一 16时53分28秒
 ************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <string.h>

#define SEM_KEY 1234
#define SHM_KEY 1235
#define SHM_SIZE (1024 * 1024)		// 1M
#define BUFFER_SIZE 1024		// 1K

struct people {
	char name[BUFFER_SIZE];
	int age;
};

union semun {
	int val;
	struct semid_ds *buf;
	ushort *array;
};

//信号量的P操作
void p(int semid)
{
	//成员一定要初始化
	struct sembuf sem_p;
	sem_p.sem_op = -1;
	sem_p.sem_num = 0;
	sem_p.sem_flg = 0;
	if (semop(semid, &sem_p, 1) == -1) {
		perror("semop");
		printf("p operation is fail!\n");
	}
}

//信号量的V操作
void v(int semid)
{
	//成员一定要初始化
	struct sembuf sem_v;
	sem_v.sem_op = 1;
	sem_v.sem_num = 0;
	sem_v.sem_flg = 0;
	if (semop(semid, &sem_v, 1) == 1) {
		perror("semop");
		printf("v operation is fail!\n");
	}
}

int main(int argc, char *argv[])
{
	int semid;
	int shmid;
	int age = 10;
	int i = 1;
	char buff[BUFFER_SIZE];
	//key_t semkey = ftok("shmmutexwrite.c", 0);
	//key_t shmkey = ftok("shmmutexread.c", 0);
	key_t semkey = SEM_KEY;
	key_t shmkey = SHM_KEY;

	//创建共享内存和信号量的IPC
	if ((semid = semget(semkey, 1, 0666 | IPC_CREAT)) == -1) {
		perror("semget");
		printf("creat sem is fail!\n");
		exit(EXIT_FAILURE);
	}

	if ((shmid = shmget(shmkey, SHM_SIZE, 0666 | IPC_CREAT)) == -1) {
		perror("shmget");
		printf("creat shm is fail!\n");
		exit(EXIT_FAILURE);
	}

	//设置信号量的初始值为1,就是资源个数
	union semun sem_u;
	sem_u.val = 1;
	semctl(semid, 0, SETVAL, sem_u);

	//将共享内存映射到当前进程的地址中,之后直接对进程中的地址addr操作
	//就是对共享内存操作
	struct people *addr = (struct people *)shmat(shmid, NULL, 0);
	if (addr == (struct people *)-1) {
		perror("shmat");
		printf("shm shmat is fail!\n");
		exit(EXIT_FAILURE);
	}

	//向共享内存写入数据
	addr->age = 0;		//第1个元素作为可供消费的产品数量不存放产品
	do {
		p(semid);	//信号值减1
		memset(buff, 0, BUFFER_SIZE);
		memset((addr + i)->name, 0, BUFFER_SIZE);
		printf("写进程:输入一些姓名(不超过%d个字符)到共享"
				"内存(输入\"quit\"退出):\n", BUFFER_SIZE);
		if (fgets(buff, BUFFER_SIZE, stdin) == NULL) {
			perror("fgets");
			printf("fgets input error!\n");
			v(semid);
			exit(EXIT_FAILURE);
		}

		strncpy((addr + i)->name, buff, strlen(buff) - 1);
		(addr + i)->age = ++age;
		addr->age++;
		i++;
		v(semid);
	} while (strncmp(buff, "quit", 4) != 0);

	if (shmdt(addr) == -1) {
		perror("shmdt");
		printf("shmdt is fail\n");
		exit(EXIT_FAILURE);
	}

	return 0;
}

读入程序:
/*************************************************************************
	> File Name: shmmutexread.c
	> Author: gwq
	> Mail: 457781132@qq.com 
	> Created Time: 2014年10月13日 星期一 16时53分28秒
 ************************************************************************/

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <string.h>
#include <stdlib.h>

#define SEM_KEY 1234
#define SHM_KEY 1235
#define SHM_SIZE (1024 * 1024)		// 1M
#define BUFFER_SIZE 1024		// 1K

struct people {
	char name[BUFFER_SIZE];
	int age;
};

//信号量的P操作
void p(int semid)
{
	//成员一定要初始化
	struct sembuf sem_p;
	sem_p.sem_op = -1;
	sem_p.sem_num = 0;
	sem_p.sem_flg = 0;
	if (semop(semid, &sem_p, 1) == -1) {
		perror("semop");
		printf("p operation is fail\n");
	}
}

//信号量的V操作
void v(int semid)
{
	//成员一定要初始化
	struct sembuf sem_v;
	sem_v.sem_num = 0;
	sem_v.sem_op = 1;
	sem_v.sem_flg = 0;
	if (semop(semid, &sem_v, 1) == 1) {
		perror("semop");
		printf("v operation is fail\n");
	}
}

int main(int argc, char *argv[])
{
	int semid;
	int shmid;
	int i = 1;
	//key_t semkey = ftok("shmmutexwrite.c", 0);
	//key_t shmkey = ftok("shmmutexread.c", 0);
	key_t semkey = SEM_KEY;
	key_t shmkey = SHM_KEY;

	//创建共享内存和信号量的IPC
	if ((semid = semget(semkey, 1, 0666)) == -1) {
		perror("semget");
		printf("creat sem is fail!\n");
		exit(EXIT_FAILURE);
	}

	if ((shmid = shmget(shmkey, 0, 0666)) == -1) {
		perror("shmget");
		printf("creat shm is fail!\n");
		exit(EXIT_FAILURE);
	}

	//将共享内存映射到当前进程的地址中,之后直接对进程中的地址addr操作
	//就是对共享内存操作
	struct people *addr;
	addr = (struct people *)shmat(shmid, NULL, 0);
	if (addr == (struct people *)-1) {
		perror("shmat");
		printf("shm shmat is fail!\n");
		exit(EXIT_FAILURE);
	}
	//从共享内存读出数据
	do {
		p(semid);
		if (addr->age > 0) {
			addr->age--;
			if (strncmp((addr + i)->name, "quit", 4) == 0) {
				break;
			}
			printf("\n读进程:绑定到共享内存%p:%d 姓名 %s,年龄 %d\n",
					addr, i, (addr + i)->name, (addr + i)->age);
			++i;
		}
		v(semid);
	} while (1);

	//将共享内存与当前进程断开
	if (shmdt(addr) == -1) {
		perror("shmdt");
		printf("shmdt is fail\n");
	}
	//IPC必须显示删除。否则会一直留存在系统中
	if (semctl(semid, 0, IPC_RMID, 0) == -1) {
		perror("semctl");
		printf("semctl delete error\n");
	}
	if (shmctl(shmid, IPC_RMID, NULL) == -1) {
		perror("shmctl");
		printf("shmctl delete error\n");
	}

	return 0;
}


写进程的作用是先创建一个共享内存,然后向里边写入内容,读进程的作用是打开一个已经存在的共享内存,从里边读取写进程写入的内容,并打印到终端上。要求是写进程先运行。
下边一一说明我遇到的问题:
一,ftok函数
就是这个函数的使用不当造成了前面提到的那个问题,写进程运行后,运行读进程报错,提示找不到共享内存。
ftok函数的作用是将给定的一个文件和子序号转换成IPC所使用的键值。而且如果两次调用ftok函数所指定的文件和子序号一样,那么返回的键值是一样的。问题就在这里,因为老师给定的程序中使用的是ftok函数获得的键值,而且指定的参数是这这两个源程序文件,更准确的说,获取共享内存的键值的参数指定的是读入程序的源文件shmmutexread.c,按道理来说,ftok返回的键值是一样的,不应该产生这样的错误,但需要注意的是ftok函数的描述是所指定的文件和子序号一样,返回的键值才是一样的,每次指定的子序号是一样的,所以错误就只可能出在所指定的文件上了,我每次的操作总是先运行写进程,然后,打开读进程后,或许经过了修改,然后再编译运行的读进程,这时能否保证ftok返回的键值和读进程中返回的是一样的?也就是说将一个文件修改过后,这个文件与原来的文件在ftok函数的意义上还是否一样。经过查资料(百度百科),我了解到,在一般的UNIX的实现中,ftok函数是将指定文件的索引节点号取出(索引节点号可以由命令ls -i查看),然后加上子序号得到一个键值返回。当删除或重建文件后, 索引节点号由操作系统根据当时文件系统的使用情况分配,因此与原来不同,所以得到的索引节点号也不同,所以修改过文件后,索引节点号一般会改变,那返回的键值也就可能不一样了,正是因为这样,才导致了前边问题的出现。所以要确保 如果要确保key_t值不变,要么确保ftok的文件不被删除,要么不用ftok,指定一个固定的key_t值,如我修改过后的程序那样,约定一个确定键值。不过,我看到一些资料说,我们不应该自己指定共享内存的键值,具体原因不太清楚,可能是因为万一你指定的键值已经与存在的共享内存关联了,那就出问题了,所以就出现了另外一种解决的办法,就是在调用ftok函数的时候,指定一个系统的关键配置文件,这个文件不存在轻易被删除或重建的的问题,这就保证了得到的键值一定是一样的。
另一个问题是子序号,man page里说子序号的最低8个有效位才会被使用,并且不能为0,这两个程序中将子序号设置成了0,暂时还没发现问题。不过我认为,可能是防止与其他进程冲突,因为如果其他进程也指定了一个与这个程序相同的系统关键配置文件,而他也将序号设成0,那么返回的键值就相同,也就冲突了。
为了验证ftok函数在指定文件重建后,返回的键值会改变,我写了如下的程序来验证,当修改程序的源文件后,有可能发现返回的键值不一样了,当然也可以指定一个其他的文件。
/*************************************************************************
	> File Name: ftok.c
	> Author: gwq
	> Mail: gwq5210@qq.com 
	> Created Time: 2014年11月26日 星期三 16时32分26秒
 ************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main(int argc, char *argv[])
{
	key_t shmkey = ftok(__FILE__, 1);
	if (shmkey == -1) {
		perror("ftok");
		exit(EXIT_FAILURE);
	}
	printf("shmkey = %d\n", shmkey);
	printf("shmkey = 0x%x\n", shmkey);

	return 0;
}

二,shmdt与shmctl函数
我在运行过程中发现的另外一个问题是:运行过程中,可能会出现错误,然后我就使用<C-c>(ctrl+c)将进程结束,并没有考虑其他释放资源的问题,因为一般来说,资源在进程结束的时候就会被操作系统自动回收。可是,我在学习这个进程间共享内存通信的时候,就发现需要使用shmdt函数先将共享内存与当前进程脱离(之前已经使用过shmat将共享内存连接到用户地址空间,然后对这个地址的操作就是对共享内存的操作),然后再使用shmctl函数将共享内存删除(要保证后续进程不再使用这个共享内存),如果不使用shmctl函数将共享内存删除,那这个共享内存就会永远留在系统中,也就造成了内存泄露的问题。
我就搜了有关共享内存的资料,发现可以使用ipcs命令查看当前系统中进程间通信的信息,包括共享内存,消息队列,信号量。加上-m来查看系统中的共享内存,我输入过后返现了如下结果:

其中nattch是连接到这个共享内存的数目,在相应的shmid_ds结构体nattch成员中存放,status的含义还不清楚,其他字段可以见名知意。
因为这都是我在调试程序的过程中产生的,也就没用了,所以就想把它们删了,经过查资料发现ipcrm命令可以删除共享内存,使用格式是ipcrm -m shmid,通过-m选项后边跟着shmid来删除这个共享内存,试过之后,发现只能删除nattch为0的,也就是没有进程连接到这个共享内存,才可以删除。删除过后如下:

剩下的就不能使用这个命令删除了,但是也不能让它们永远停留在内存中呀,所以我就想到了用程序来实现,用shmctl函数将共享内存的nattch的值也就是进程连接共享内存的连接数设成0,然后使用shmctl函数删除这个共享内存,我的程序如下,接收一个或多个参数,参数应该是存在的共享内存号,而且运行这个程序的用户必须有足够的权限才能删除成功,不过删除之前要确保这个共享内存没有其他进程使用了,代码如下:
/*************************************************************************
	> File Name: shmdt.c
	> Author: gwq
	> Mail: gwq5210@qq.com 
	> Created Time: 2014年11月26日 星期三 16时03分22秒
 ************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define MAX_LEN 1024

/*
 * 接收一个或多个共享内存号,将其删除
 */
int main(int argc, char *argv[])
{
	if (argc <= 1) {
		printf("Usage: ./shmdt shmid ...\n");
		return 0;
	}

	int i = 0;
	int cnt = argc - 1;
	int shmids[MAX_LEN];
	for (i = 0; i < cnt; ++i) {
		sscanf(argv[i + 1], "%d", &shmids[i]);
	}
	printf("删除之前:\n");
	system("ipcs -m");
	for (i = 0; i < cnt; ++i) {
		char *p = (char *)shmat(shmids[i], NULL, 0);
		if (p == (char *)-1) {
			perror("shmat");
			exit(EXIT_FAILURE);
		}
		if (shmdt(p) == -1) {
			perror("shmdt");
			exit(EXIT_FAILURE);
		}
		struct shmid_ds val;
		val.shm_nattch = 0;		// 将连接数设置成0
		if (shmctl(shmids[i], IPC_SET, &val) == -1) {
			perror("shmctl");
			exit(EXIT_FAILURE);
		}
		if (shmctl(shmids[i], IPC_RMID, NULL) == -1) {
			perror("shmctl");
			exit(EXIT_FAILURE);
		}
	}
	printf("删除之后:\n");
	system("ipcs -m");

	return 0;
}

下面说说shmdt和shmctl的区别,shmdt函数的作用是共享内存从用户空间中脱离(detach)出来,并没有真正删除这个共享内存块;而shmctl函数指定了IPC_RMID后,是将内存块删除,释放了资源,删除后,若有进程尝试使用shmat函数连接这个共享内存,就会失败。
三,semget函数
在没有运行写进程时,运行读进程,出现信号量不存在的提示,semget函数只有当key的值为IPC_PRIVATE或与key关联的信号量不存在并且函数参数指定了IPC_CREAT时才会创建一个新的信号量,否则就是打开一个信号量,在读进程中,不满足以上 条件所以就是打开一个信号量,因为写进程没有运行,那这个信号量也就不会存在,当然就打开失败。对于semget函数,因为参数也是ftok函数的返回值,所以同样可能会出现与shmget的情况,也需要注意,否则,就会产生错误。
四,semop函数
这个问题是我先运行写进程,但是没有写入数据,会出现p操作失败的提示,有时先写入了数据,也会出现,我在读程序的p操作前边打印一个点(.),结果就不会出现p操作失败了,这个错误有时有,有时没有,基本上是偶尔出现的,所以我想就应该与数组越界或变量未初始化有关,果然,在原来的程序中semop函数的第二个参数中有一个成员没有初始化,第二个参数是一个结构体struct sembuf数组,这个结构体定义在<sys/sem.h>文件中,如下:
/* semop system calls takes an array of these. */
struct sembuf {
	unsigned short  sem_num;	/* semaphore index in array */
	short		sem_op;		/* semaphore operation */
	short		sem_flg;	/* operation flags */
};
其中,第一个成员是要修改的信号量在这个信号量集中的编号(从0开始),第二个是对这个信号量的操作,有三种,第三个是标志,有两个取值IPC_NOWAIT和 SEM_UNDO,如果指定了前者,当某个信号量的资源不足时进行P操作,此时不会阻塞等待,而是直接返回资源不可用的错误;不是如果指定了后者,那么在程序终止的时候会自动撤销对信号量的操作。就是因为第三个参数没有指定,导致其值是不确定的,刚好就取值到了IPC_NOWAIT,所以,就出现了p操作错误的现象,初始化后,这种现象消失了。对于任何变量一定要初始化,而不能假定其被初始化了。
五,出错情况没有删除共享内存的问题
这两个程序中删除共享内存是在读入程序中的,如果在删除之前发生了错误,就没有删掉共享内存,就造成了内存泄露,暂时还没有想到比较好的方法,可以手动删除。

以上就所我在学习linux进程间使用共享内存通信的总结。
上边用到的系统调用可以参考这篇博客:linux常用系统调用简介
参考:
http://www.dcshi.com/?p=79
http://baike.baidu.com/item/ftok?fr=aladdin
http://blog.csdn.net/fan_hai_ping/article/details/6682046
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值