Linux进程间通信--共享内存示例(信号量保证同步)

在Linux系统中,每个进程都有独立的虚拟内存空间,也就是说不同的进程访问同一段虚拟内存地址所得到的数据是不一样的,这是因为不同进程相同的虚拟内存地址会映射到不同的物理内存地址上。但有时候为了让不同进程之间进行通信,需要让不同进程共享相同的物理内存,Linux通过《共享内存》来实现这个功能。下面先来介绍一下Linux系统的共享内存的使用。

一、共享内存函数介绍:

以下介绍摘自csdn及其他论坛,仅为学习之用。(部分信息反复ctrl+cv,已经漏掉且错误,在此更正)

1、shmget函数

该函数用来创建共享内存,它的原型为:

int shmget(key_t key, size_t size, int shmflg);

解析:

与信号量的semget函数一样,程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1.

不相关的进程可以通过该函数的返回值访问同一共享内存,它代表程序可能要使用的某个资源,程序对所有共享内存的访问都是间接的,程序先通过调用shmget函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget函数的返回值),只有shmget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。

第二个参数,size以字节为单位指定需要共享的内存容量;

第三个参数,shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存;

2、shmat函数

第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。它的原型如下:

void *shmat(int shm_id, const void *shm_addr, int shmflg);

解析:

第一个参数,shm_id是由shmget函数返回的共享内存标识。

第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。

第三个参数,shm_flg是一组标志位,通常为0。

调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.

3、shmdt函数

该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。它的原型如下:

int shmdt(const void *shmaddr);

解析:参数shmaddr是shmat函数返回的地址指针,调用成功时返回0,失败时返回-1.

4、shmctl函数

与信号量的semctl函数一样,用来控制共享内存,它的原型如下:

int shmctl(int shm_id, int command, struct shmid_ds *buf);

解析:

第一个参数,shm_id是shmget()函数返回的共享内存标识符。

第二个参数,command是要采取的操作,它可以取下面的三个值 :

  • IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
  • IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
  • IPC_RMID:删除共享内存段

第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。

shmid_ds结构 至少包括以下成员:

struct shmid_ds
{
    uid_t shm_perm.uid;
    uid_t shm_perm.gid;
    mode_t shm_perm.mode;
};

二、用法示例:

test.c(gcc test.c -o test)

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define SHM_SIZE 128
 
union semun
{
	int val;
	struct semid_ds *buf;
	unsigned short *arry;
};
 
static int sem_id = 0;
int real_i = 0;
 
static int set_semvalue();
static void del_semvalue();
static int semaphore_p();
static int semaphore_v();
 
int main(int argc, char *argv[])
{
	int shm_id = 0;
	char message = 'X';
	char *share;
	int i = 0;
 
	//创建信号量
	sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
	
	//1.创建共享内存,返回一个标识
    shm_id = shmget((key_t)1234, SHM_SIZE, IPC_CREAT|0666);
    if(shm_id == -1){
        perror("shmget()");
    }
	//2.将创建的共享内存与虚拟地址进行关联
    share = shmat(shm_id, NULL, 0);
 
	if(argc > 1)
	{
		//程序第一次被调用,初始化信号量
		if(!set_semvalue())
		{
			fprintf(stderr, "Failed to initialize semaphore\n");
			exit(EXIT_FAILURE);
		}
		message = argv[1][0];
		sleep(2);
	}
	for(i = 0; i < 10; ++i)
	{
		//进入临界区
		//if(!semaphore_p())
		//	exit(EXIT_FAILURE);
		printf("%c", message);
		fflush(stdout);
		sprintf(share+strlen(share), "%c", message);
		real_i += 1;
		sleep(rand() % 3);
		sprintf(share+strlen(share), "%c", message);
		real_i += 1;
		//离开临界区,休眠随机时间后继续循环
		printf("%c", message);
		fflush(stdout);
		//if(!semaphore_v())
		//	exit(EXIT_FAILURE);
		sleep(rand() % 2);
	}
 
	sleep(10);
	printf("\n %s - finished\n", share);
 
	if(argc > 1)
	{
		//如果程序是第一次被调用,则在退出前删除信号量
		sleep(3);
		del_semvalue();
	}
	
	//3.把共享内存从当前进程中分离
    shmdt(share);
	//4.删除共享内存
    shmctl(shm_id, IPC_RMID, 0);
	
	return 0;
}
 
static int set_semvalue()
{
	//用于初始化信号量,在使用信号量前必须这样做
	union semun sem_union;
 
	sem_union.val = 1;
	if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
		return 0;
	return 1;
}
 
static void del_semvalue()
{
	//删除信号量
	union semun sem_union;
 
	if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
		fprintf(stderr, "Failed to delete semaphore\n");
}
 
static int semaphore_p()
{
	//对信号量做减1操作,即等待P(sv)
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = -1;//P()
	sem_b.sem_flg = SEM_UNDO;
	if(semop(sem_id, &sem_b, 1) == -1)
	{
		fprintf(stderr, "semaphore_p failed\n");
		return 0;
	}
	return 1;
}
 
static int semaphore_v()
{
	//这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = 1;//V()
	sem_b.sem_flg = SEM_UNDO;
	if(semop(sem_id, &sem_b, 1) == -1)
	{
		fprintf(stderr, "semaphore_v failed\n");
		return 0;
	}
	return 1;
}

运行结果如下:

Linux进程间通信--共享内存示例(信号量保证同步)_第1张图片

假如我们在操作共享内存的时候,需要保证一个进程写入完数据后,另外一个进程才可以写入数据,即保证“X”和“O”是成对出现的;但是两个进程在操作共享内存的时候,显然没有什么规律。

共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取。所以我们通常需要用其他的机制来同步对共享内存的访问,例如前面说到的信号量。有关信号量的更多内容,

可以查阅我的另一篇文章:

Linux进程间通信——使用信号量

linux学习之二十一---信号量

三、解决办法(使用信号量):

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define SHM_SIZE 128
 
union semun
{
	int val;
	struct semid_ds *buf;
	unsigned short *arry;
};
 
static int sem_id = 0;
int real_i = 0;
 
static int set_semvalue();
static void del_semvalue();
static int semaphore_p();
static int semaphore_v();
 
int main(int argc, char *argv[])
{
	int shm_id = 0;
	char message = 'X';
	char *share;
	int i = 0;
 
	//创建信号量
	sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
	
	//1.创建共享内存,返回一个标识
    shm_id = shmget((key_t)1234, SHM_SIZE, IPC_CREAT|0666);
    if(shm_id == -1){
        perror("shmget()");
    }
	//2.将创建的共享内存与虚拟地址进行关联
    share = shmat(shm_id, NULL, 0);
 
	if(argc > 1)
	{
		//程序第一次被调用,初始化信号量
		if(!set_semvalue())
		{
			fprintf(stderr, "Failed to initialize semaphore\n");
			exit(EXIT_FAILURE);
		}
		message = argv[1][0];
		sleep(2);
	}
	for(i = 0; i < 10; ++i)
	{
		//进入临界区
		if(!semaphore_p())
			exit(EXIT_FAILURE);
		printf("%c", message);
		fflush(stdout);
		sprintf(share+strlen(share), "%c", message);
		real_i += 1;
		sleep(rand() % 3);
		sprintf(share+strlen(share), "%c", message);
		real_i += 1;
		//离开临界区,休眠随机时间后继续循环
		printf("%c", message);
		fflush(stdout);
		if(!semaphore_v())
			exit(EXIT_FAILURE);
		sleep(rand() % 2);
	}
 
	sleep(10);
	printf("\n %s - finished\n", share);
 
	if(argc > 1)
	{
		//如果程序是第一次被调用,则在退出前删除信号量
		sleep(3);
		del_semvalue();
	}
	
	//3.把共享内存从当前进程中分离
    shmdt(share);
	//4.删除共享内存
    shmctl(shm_id, IPC_RMID, 0);
	
	return 0;
}
 
static int set_semvalue()
{
	//用于初始化信号量,在使用信号量前必须这样做
	union semun sem_union;
 
	sem_union.val = 1;
	if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
		return 0;
	return 1;
}
 
static void del_semvalue()
{
	//删除信号量
	union semun sem_union;
 
	if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
		fprintf(stderr, "Failed to delete semaphore\n");
}
 
static int semaphore_p()
{
	//对信号量做减1操作,即等待P(sv)
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = -1;//P()
	sem_b.sem_flg = SEM_UNDO;
	if(semop(sem_id, &sem_b, 1) == -1)
	{
		fprintf(stderr, "semaphore_p failed\n");
		return 0;
	}
	return 1;
}
 
static int semaphore_v()
{
	//这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = 1;//V()
	sem_b.sem_flg = SEM_UNDO;
	if(semop(sem_id, &sem_b, 1) == -1)
	{
		fprintf(stderr, "semaphore_v failed\n");
		return 0;
	}
	return 1;
}

执行结果:

Linux进程间通信--共享内存示例(信号量保证同步)_第2张图片

例子分析 :同时运行一个程序的两个实例,注意第一次运行时,要加上一个字符作为参数,例如本例中的字符‘O’,它用于区分是否为第一次调用,同时这个字符输出到屏幕中。因为每个程序都在其进入临界区后和离开临界区前打印一个字符,所以每个字符都应该成对出现,正如你看到的上图的输出那样。在main函数中循环中我们可以看到,每次进程要访问stdout(标准输出),即要输出字符时,每次都要检查信号量是否可用(即stdout有没有正在被其他进程使用)。所以,当一个进程A在调用函数semaphore_p进入了临界区,输出字符后,调用sleep时,另一个进程B可能想访问stdout,但是信号量的P请求操作失败,只能挂起自己的执行,当进程A调用函数semaphore_v离开了临界区,进程B马上被恢复执行。然后进程A和进程B就这样一直循环了10次。

四、使用共享内存的优缺点

1、优点:我们可以看到使用共享内存进行进程间的通信真的是非常方便,而且函数的接口也简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,也加快了程序的效率。同时,它也不像匿名管道那样要求通信的进程有一定的父子关系。

2、缺点:共享内存没有提供同步的机制,这使得我们在使用共享内存进行进程间通信时,往往要借助其他的手段来进行进程间的同步工作。

五、参考资料:

Linux进程间通信——使用信号量

进程间通信系列 之 共享内存及其实例

你可能感兴趣的:(Linux-C编程,linux,共享内存,信号量)