linux系统编程 进程间通信

一、进程间通信

(1)进程间通信的原理

		尽管进程空间是各自独立的,相互之间没有任何可以共享的空间,但是至少还有一样东西是所有
	进程所共享的,那就是OS,因为甭管运行有多少个进程,但是它们共用OS只有一个。
		
		既然大家共用的是同一个OS,那么显然,所有的进程可以通过大家都共享第三方OS来实现数据的
	转发。
	
		因此进程间通信的原理就是,OS作为所有进程共享的第三方,会提供相关的机制,以实现进程间数
	据的转发,达到数据共享的目的。

		
(2)广义上的进程间通信

	其实广义上来说,任何一种能够实现进程间数据交换的方式,都可以被称为进程间通信,比如
		
		A进程——————文件———————B进程
		
		A进程—————数据库——————B进程
		
		
		不过一般来说,这种广义的进程间通信,并不被算作真正的“进程间通信”。
		只有OS所提供的专门的通信机制,才能算作是真正的“进程间通信”,我们本章所讲的就是狭义上
	的真正的“进程间通信”。
		
			
(3)Linux提供的“进程通信”方式有哪些

		Linux的父亲是Unix,所以Linux的进程间通信,其实都是继承于Unix。
		
	不管继承自谁,Linux所提供的进程间通信机制到底有哪些呢?
			
			
		1)信号
				上一章讲的信号其实也是进程间通信的一种,只不过信号是非精确通信,而本章讲的
			IPC是精确通信。所谓精确通信,就是能告诉你详细信息,而信号这种非精确通信,只能
			通知某件事情发生了,但是无法告诉详细信息。
				
				
		2)本章的进程间通信
			(a)管道
					· 无名管道
					· 有名管道
						
					OS在进程之间建立一个“管道”,通过这个管道来实现进程间数据的交换。
						
			(b)system V IPC
					· 消息队列:通过消息队列来通信
					· 共享内存:通过共享内存来通信
					· 信号量:借助通信来实现资源的保护(一种加锁机制)
					

		3)域套接字
				讲网络编程时再介绍。

二、无名管道

2.1 无名管道的通信原理

	具体来说就是,内核会开辟一个“管道”,通信的进程通过共享这个管道,从而实现通信。
	
	
(1)到底什么是管道
			内核的代码也是运行在物理内存上的,内核创建一个“管道”,其实就是在内核自己所在的物理
		内存空间中开辟出一段缓存空间,比如char buf[1024];
			
			

(2)如何操作无名管道

			以文件的方式来读写管道,以文件方式来操作时
			
			1)有读写用的文件描述符
			2)读写时会用write、read等文件Io函数。
			
			
			
(3)为什么叫无名管道
			既然可以通过“文件描述符”来操作管道,那么它就是一个文件(管道文件),但是无名管道
		文件比较特殊,它没有文件名,正是因为没有文件名,所有被称为无名管道。
			
			没有文件名,我们怎么操作这个文件呢?
			后面再讲这个问题。

2.2 无名管道的API

2.2.1 函数原型

	#include <unistd.h>
	
	int pipe(int pipefd[2]);
	
(1)功能
		创建一个用于亲缘进程(父子进程)之间通信的无名管道(缓存),并将管道与两个读写文件描述符
	关联起来。
	
		无名管道只能用于亲缘进程之间通信,为什么只能用于亲缘进程之间通信呢?
		后面再详细介绍。
			

(2)参数:缓存地址,缓存用于存放读写管道的文件描述符。

			从这个参数的样子可以看出,这个缓存就是一个拥有两个元素的int型数组。
			
		1)元素[0]:里面放的是读管道的读文件描述符
		2)元素[1]:里面放的是写管道的写文件描述符。
		
		
			特别需要注意的是,这里的读和写文件描述符,是两个不同的文件描述符。
		
			从这里大家也可以看出,并不是所有的文件描述符,都是通过open函数打开文件得到的。
		这里无名管道的读、写文件描述符,就是直接在创建管道时得到的,与open没有任何关系。
		
			而且这里也根本没办法使用open函数,因为open函数需要文件路径名,无名管道连文件名
		都没有,所以说根本就没办法使用open来打开文件,返回文件描述符。
	
	
(3)返回值:成功返回0,失败则返回-1,并且errno被设置。

2.2.2 无名管道特点

(1)无名管道只能用于亲缘进程之间通信,为什么?
			
	由于没有文件名,因此进程没办法使用open打开管道文件,从而得到文件描述符,所以只有一种办法,
那就是父进程先调用pipe创建出管道,并得到读写管道的文件描述符。

	然后再fork出子进程,让子进程通过继承父进程打开的文件描述符,父子进程就能操作同一个管道,
从而实现通信。

图:
在这里插入图片描述

	对子进程继承父进程属性这一点不清楚的同学,说明“进程控制”这一章你没有学好,你需要回去
复习一下。

	
	通过前面的描述,我们自然就能理解,为什么无名管道只能用于亲缘进程之间通信了。
	
	什么样的进程之间,我们可以称为亲缘进程呢?
	只要是存在继承关系的进程就是亲缘进程,继承关系分为两种。
	
	(1)直接继承关系
			父进程————>子进程
	
	(2)间接继承关系
			父进程————>子进程————>子进程————>...

				
				
(2)读管道时,如果没有数据的话,读操作会休眠(阻塞)

2.2.3 代码演示

2.2.3.1 父子进程单向通信

实现步骤:

(a)父进程在fork之前先调用pipe创建无名管道,并获取读、写文件描述符
(b)fork创建出子进程,子进程继承无名管道读、写文件描述符
(c)父子进程使用各自管道的读写文件描述符进行读写操作,即可实现通信

代码演示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>

void print_err(char *estr)
{
	perror(estr);
	exit(-1);
}

int main(void)
{
	int ret = 0;
	//[0]:读文件描述符
	//[1]:写文件描述符
	int pipefd[2] = {0};//用于存放管道的读写文件描述符
	
	ret = pipe(pipefd);
	if(ret == -1) print_err("pipe fail");

	ret = fork();
	if(ret > 0)
	{	
		close(pipefd[0]);	
		while(1)
		{
			write(pipefd[1], "hello", 5);				
			sleep(1);
		}
	}
	else if(ret == 0)
	{
		close(pipefd[1]);
		while(1)
		{
			char buf[30] = {0};
			bzero(buf, sizeof(buf));
			read(pipefd[0], buf, sizeof(buf));
			printf("child, recv data:%s\n", buf);
		}	
	}

	return 0;
}


	为了避免干扰,通常会把没有使用的文件描述关闭。

SIGPIPE信号:

(a)我们讲信号时介绍过这个信号,这里再说一说这个信号,为什么讲这个信号?
						· 与管道有关
						· 回顾信号的内容,进行知识的综合运用
			
			
(b)什么时候会产生在这个信号?

			写管道时,如果管道的读端被close了话,向管道“写”数据的进程会被内核发送一个
		SIGPIPE信号,发这个信号的目的就是想通知你,管道所有的“读”都被关闭了。
		
			这就好比别人把水管的出口(读)给堵住了,结果你还一直往里面灌水(写),别人肯定
		会警告你,因为你这样可能会对水管造成损害,道理其实是类似的。
			
			由于这个信号的默认动作是终止,所以收到这个信号的进程会被终止,如果你不想被终止的
		话,你可以忽略、捕获、或者屏蔽这个信号。

			只有当管道所有的读端都被关闭时,才会产生这个信号,只有还有一个读端开着,就不会产生。
2.2.3.2 父子进程双向通信

1)单个无名管道无法实现双向通信,为什么?

	因为使用单个无名管道来实现双向通信时,自己发送给对方的数据,就被自己给抢读到。

2)如何实现无名管来实现双向通信

	使用两个无名管道,每个管道负责一个方向的通信。

代码演示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>

void print_err(char *estr)
{
	perror(estr);
	exit(-1);
}

int main(void)
{
	int ret = 0;
	//[0]:读文件描述符
	//[1]:写文件描述符
	int pipefd1[2] = {0};//用于存放管道的读写文件描述符
	int pipefd2[2] = {0};//用于存放管道的读写文件描述符
	
	ret = pipe(pipefd1);
	if(ret == -1) print_err("pipe fail");
	ret = pipe(pipefd2);
	if(ret == -1) print_err("pipe fail");

	ret = fork();
	if(ret > 0)
	{	
		close(pipefd1[0]);
		close(pipefd2[1]);
		char buf[30] = {0};
		while(1)
		{
			write(pipefd1[1], "hello", 5);				
			sleep(1);

			bzero(buf, sizeof(buf));
			read(pipefd2[0], buf, sizeof(buf));
			printf("parent, recv data:%s\n", buf);
		}
	}
	else if(ret == 0)
	{
		close(pipefd1[1]);
		close(pipefd2[0]);
		char buf[30] = {0};
		while(1)
		{
			sleep(1);	
			write(pipefd2[1], "world", 5);

			bzero(buf, sizeof(buf));
			read(pipefd1[0], buf, sizeof(buf));
			printf("child, recv data:%s\n", buf);
		}	
	}

	return 0;
}



2.3 无名管道有两个缺点

(1)无法用于非亲缘进程之间
		因为非亲缘进程之间没办法继承管道的文件描述符。
				
(2)无法实现多进程之间的网状通信

		如果非要使用无名管道实现多进程之间的网状通信的话,文件描述符的继承关系将非常的复杂。
	所以无名管道基本只适合两个进程间的通信。

2.4 什么时候合适使用无名管道呢?

如果通信的进程只有两个,而且还是亲缘进程时,那么可以使用无名管道来通信。
比如:

1)直接继承父子进程之间的通信

	父进程 ————————————————> 子进程
		|											 	  |
		|—————————无名管道————————|
	
	
2)间接继承关系的两进程之间的通信			

	父进程 ——————> 子进程 ——————> 子进程 ———————> 子进程 
		|						|
		|————————————————————无名管道———————————————————|

三、有名管道

3.1 为什么叫“有名管道”

	无名管道因为没有文件名,被称为了无名管道,同样的道理,有名管道之所以叫“有名管道”,是因为
它有文件名。
		
	也就是说当我们调用相应的API创建好“有名管道”后,会在相应的路径下面看到一个叫某某名字的
“有名管道文件”。
	
	不管是有名管道,还是无名管道,它们的本质其实都是一样的,它们都是内核所开辟的一段缓存空间。
进程间通过管道通信时,本质上就是通过共享操作这段缓存来实现,只不过操作这段缓存的方式,是以
读写文件的形式来操作的。

3.2 有名管道特点

1)能够用于非亲缘进程之间的通信
		
	因为有文件名,所以进程可以直接调用open函数打开文件,从而得到文件描述符,不需要像无名管道
一样,必须在通过继承的方式才能获取到文件描述符。
	
	所以任何两个进程之间,如果想要通过“有名管道”来通信的话,不管它们是亲缘的还是非亲缘的,只要
调用open函数打开同一个“有名管道”文件,然后对同一个“有名管道文件”进行读写操作,即可实现通信。
	
	A进程 —————————> 有名管道 ————————> B进程
	
	总之,不管是亲缘进程还是非亲缘进程,都可以使用有名管道来通信。
		
		
2)读管道时,如果管道没有数据的话,读操作同样会阻塞(休眠)
		
		
3)当进程写一个所有读端都被关闭了的管道时,进程会被内核返回SIGPIPE信号
		如果不想被该信号终止的话,我们需要忽略、捕获、屏蔽该信号。
		
		不过一般情况下,不需要对这个信号进行处理,除非你有必须要处理的理由。

3.3 有名管道的使用步骤

(1)进程调用mkfifo创建有名管道

(2)open打开有名管道

(3)read/write读写管道进行通信
		
	对于通信的两个进程来说,创建管道时,只需要一个人创建,另一个直接使用即可。
	
	为了保证管道一定被创建,最好是两个进程都包含创建管道的代码,谁先运行就谁先创建,后运行的
发现管道已经创建好了,那就直接open打开使用。

3.4 有名管道API

3.4.1 函数原型

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
		
(1)功能
		创建有名管道文件,创建好后便可使用open打开。

		如果是创建普通文件的话,我们可以使用open的O_CREAT选项来创建,比如:
			open("./file", O_RDWR|O_CREAT, 0664);
		
		但是对于“有名管道”这种特殊文件,这里只能使用mkfifo函数来创建。


(2)参数
	1)pathname:被创建管道文件的文件路径名。
	
	2)mode:指定被创建时原始权限,一般为0664(110110100),必须包含读写权限。
				
				使用open函数创建普通文件时,指定原始权限是一样的。
					open("./file", O_RDWR|O_CREAT, 0664);
	
					不过我们学习第2章时讲过,创建新文件时,文件被创建时的真实权限=mode & 
				(~umask)umask是文件权限掩码,一般默认为002或者022,对umask不清楚的同学
				,请会看第2章。
		
			mkfifo(“./fifo”, 0664);
	
(3)返回值:成功返回0,失败则返回-1,并且errno被设置。	

3.4.2 代码演示

3.4.2.1 单向通信

mkfifo1.c代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>


#define FIFONAME1 "./fifo1"
#define FIFONAME2 "./fifo2"

void print_err(char *estr)
{	
	perror(estr);
	exit(-1);
}	

int creat_open_fifo(char *fifoname, int open_mode)
{	
	int ret = -1;
	int fd = -1;	
	
	ret = mkfifo(fifoname, 0664);
	//如果mkfifo函数出错了,但是这个错误是EEXIST,不报这个错误(忽略错误)
	if(ret == -1 && errno!=EEXIST) print_err("mkfifo fail");	
	
	fd = open(fifoname, open_mode);
	if(fd == -1) print_err("open fail");

	return fd;
}

void signal_fun(int signo)
{
	//unlink();
	remove(FIFONAME1);
	exit(-1);
}
		
int main(void)
{
	char buf[100] = {0};
	int ret = -1;
	int fd1 = -1;


	fd1 = creat_open_fifo(FIFONAME1, O_WRONLY);
	signal(SIGINT, signal_fun);
	while(1)
	{
		bzero(buf, sizeof(buf));
		scanf("%s", buf);
		write(fd1, buf, sizeof(buf));	
	}

	return 0;
}	

mkfifo2.c代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>


#define FIFONAME1 "./fifo1"
#define FIFONAME2 "./fifo2"

void print_err(char *estr)
{	
	perror(estr);
	exit(-1);
}	

int creat_open_fifo(char *fifoname, int open_mode)
{	
	int ret = -1;
	int fd = -1;	
	
	ret = mkfifo(fifoname, 0664);
	//如果mkfifo函数出错了,但是这个错误是EEXIST,不报这个错误(忽略错误)
	if(ret == -1 && errno!=EEXIST) print_err("mkfifo fail");	
	
	fd = open(fifoname, open_mode);
	if(fd == -1) print_err("open fail");

	return fd;
}

void signal_fun(int signo)
{
	//unlink();
	remove(FIFONAME1);
	exit(-1);
}
		
int main(void)
{
	char buf[100] = {0};
	int ret = -1;
	int fd1 = -1;


	fd1 = creat_open_fifo(FIFONAME1, O_RDONLY);
	signal(SIGINT, signal_fun);
	while(1)
	{
		bzero(buf, sizeof(buf));
		read(fd1, buf, sizeof(buf));
		printf("%s\n", buf);
	}
	return 0;
}	
3.4.2.2 双向通信
	同样的,使用一个“有名管道”是无法实现双向通信的,因为也涉及到抢数据的问题。
所以双向通信时需要两个管道。

3.5 什么时候使用有名管道

(1)实现网状通信
			面对众多进程网状通信,有名管道依然实现起来很吃力,所以基本也只适合于两个进程之间
		的通信。你自己可以尝试下,看看能不能使用有名管道来实现多进程的网状通信,在实现过程
		中,你自己就会发现,实现起来很困难。
	
	
(2)什么时候合适使用有名管道

		当两个进程需要通信时,不管是亲缘的还是非亲缘的,我们都可以使用有名管道来通信。
		
		至于亲缘进程,你也可以选择前面讲的无名管道来通信。

四、多进程与多线程

(1)回顾有名管道双向通信
		
	在使用有名管道实现双向通信时,由于读管道是阻塞读的,为了不让“读操作”阻塞“写操作”,使用了
父子进程来多线操作,
	1)父进程这条线:读管道1
	2)子进程这条线:写管道2

	实际上我们后面学习了线程以后,凡是涉及到多线操作的,基本都使用多线程来实现,比如
	1)主线程:读管道1
	2)次线程:写管道2
	
(2)对比多进程和多线程各自使用的场合		

		我们前面讲了进程,虽然线程还没有讲,但是大家大致也能理解线程是一个什么样的东西,事实上
	线程和进程都是并发运行的,但是线程和进程各自的使用的场合有所不同。
	
	
	1)线程
			凡是涉及多线时,我们使用线程来并发实现,比如我们讲的“有名管道”双向通信的例子,这
		个多线操作理论上就应该使用多线程来实现,只不过我们还没讲多线程而已。
			
			因为多线使用线程更省计算机cpu和内存的开销。
					
			也就是说创建出并发运行次线程的目的,是为了多线操作。

			
	2)进程
		一般情况下,我们的程序并不会涉及到多进程,当涉及多线操作时,我们会直接使用线程来并发
	实现。

		
	(a)那什么时候我们的程序才会涉及到多进程呢?
	
			一个简单的判断标准就是,如果你发现你的程序必须要去运行一个新程序时,此时必须
		涉及到多进程,因为此时如果你不创建一个子进程,你是没有办法来执行新程序的。
			
			
			新创建的子进程和父进程肯定是并发运行的,只不过这里并发运行的主要目的并不是
		为了多线操作,而是为了单独的去执行新程序,执行新程序时,我们只能使用多进程来操作
		,你是没有办法使用多线程来操作的,因为线程是不可能去执行一个新程序的。
			
			
	(b)一般开发的应用程序不涉及执行新程序
	
			除非你开发的是比较大型框架,或者拥有众多功能套件的大型应用软件,在你的程序中必
		须开辟新的子进程去执行具有独立功能的新程序,否则们自己写的程序一般都是单进程,根本
		不涉及开辟一个并发运行的子进程,然后在子进程里面去执行新程序。
		
			也就是说创建一个并发执行的子进程的目的,是为了执行一个全新的程序。

五、System V IPC

5.1 有关System V IPC

(1)什么是System V IPC

	前面讲的无名管道和有名管道,都是UNIX系统早期提供的比较原始的一种进程间通信(IPC)方式,
早到Unix系统设计之初就有了。
	
	后来Unix系统升级到第5版本时,又提供了三种新的IPC通信方式,分别是:
	· 消息队列
	· 信号量
	· 共享内存
	
	System V就是系统第5版本的意思,后来的Linux也继承了unix的这三个通信方式,Unix是非常
早期的而且非常优秀OS,所以其它os也借鉴了这三种的System V IPC。
				
		
(2)System V IPC的特点

	1)管道(原始IPC)
			管道的本质就是一段缓存,不过Linux OS内核是以文件的形式来管理的,所以我们操作管
		道时,不管是无名管道,还是有名管道,我们都是使用文件描述符以文件的形式来操作的。
		
		
			所以我们操作管道时,除了pipe和mkfifo这两个函数外,其它的像read、write、open
		都是我们第1章所学的文件io函数,所以大家在学习管道时,会觉得比较容易。
		
		
	2)System V IPC
	
			System V IPC与管道有所不同,它完全使用了不同的实现机制,与文件没任何的关系,
		也就是说内核不再以文件的形式来管理System V IPC,所以不能再使用文件的方式来操作。
			
			
			对于System V IPC,OS内核提供了全新的API,对于这些API来说,我们的要求是理解而
		不是记忆,因为在以后的开发中,确实用的不多。就算真的用到了,只要你理解了,你自然
		能够很快的用起来。
			
			
			
	3)使用System V IPC时,不存在亲缘进程一说
	
			任何进程之间通信时,都可以使用System V IPC来通信。
		
		
		
(3)System V IPC标识符

		我们前面说过,System V IPC不再以文件的形式存在,因此没有文件描述符这个东西,但是它
	有类似的“标识符”。
	
		你完全可以认为这个“标识符”就是文件描述符的替代者,但是它是专门给System V IPC使用的,
	所以我们不能使用文件IO函数来操作“标识符”,只能使用System V IPC的特有API才能操作。
	
	
	1)怎么才能得到这个“标识符”
			调用某API创建好某个“通信结构”以后,API就会返回一个唯一的“标识符”。
			
			比如创建好了一个“消息队列”后,创建的API就会返回一个唯一标识消息队列的“标识符”。
		
		
		
	2)System V IPC标识符的作用?
	
			比如,如果创建的是消息队列的话,进程通过消息队列唯一的标识符,就能找到创建好的
		“消息队列”,使用这个消息队列,进程就能读写数据,然后实现进程间通信。

5.2 System V IPC 之 消息队列

5.2.1 消息队列的原理

(1)消息队列的本质
		消息队列的本质就是由内核创建的用于存放消息的链表,由于是存放消息的,所以我们就把这个链表
	称为了消息队列。通信的进程通过共享操作同一个消息队列,就能实现进程间通信。
			
(2)消息是如何存放在消息队列中的呢?
			
		消息队列这个链表有很多的节点,链表上的每一个节点就是一个消息。

图:
在这里插入图片描述

		从图中可以看出,每个消息由两部分组成,分别是消息编号(消息类型)和消息正文。
		1)消息编号:识别消息用
		2)消息正文:真正的信息内容
			
			
(3)收发数据的过程

	1)发送消息
	
	(a)进程先封装一个消息包
	
				这个消息包其实就是如下类型的一个结构体变量,封包时将消息编号和消息正文
			写到结构体的成员中。
				struct msgbuf
				{
							long mtype;         /* 放消息编号,必须> 0 */
							char mtext[msgsz];  /* 消息内容(消息正文) */
				};	
			
	(b)调用相应的API发送消息
	
				调用API时通过“消息队列的标识符”找到对应的消息队列,然后将消息包发送给消息队列
			,消息包(存放消息的结构体变量)会被作为一个链表节点插入链表。
				
				
	2)接收消息
	
		调用API接收消息时,必须传递两个重要的信息,
		(a)消息队列标识符
				
		(b)你要接收消息的编号
		
				有了这两个信息,API就可以找到对应的消息队列,然后从消息队列中取出你所要编号
			的消息,如此就收到了别人所发送的信息。
	
		
	“消息队列”有点像信息公告牌,发送信息的人把某编号的消息挂到公告牌上,接收消息的人自己到公
告牌上去取对应编号的消息,如此,发送者和接受者之间就实现了通信。
		
		
(4)使用消息队列实现网状交叉通信

			对于前面讲的管道来说,很难实现网状交叉通信,但是使用消息队列确非常容易实现。

5.2.2 消息队列的使用步骤

(1)使用msgget函数创建新的消息队列、或者获取已存在的某个消息队列,并返回唯一标识消息队列的
				标识符(msqID),后续收发消息就是使用这个标识符来实现的。
			
			
(2)收发消息
		· 发送消息:使用msgsnd函数,利用消息队列标识符发送某编号的消息
		·	接收消息:使用msgrcv函数,利用消息队列标识符接收某编号的消息
		
		
(3)使用msgctl函数,利用消息队列标识符删除消息队列


	对于使用消息队列来通信的多个进程来说,只需要一个进程来创建消息队列就可以了,对于其它要参与
通信的进程来说,直接使用这个创建好的消息队列即可。

	为了保证消息队列的创建,最好是让每一个进程都包含创建消息队列的代码,谁先运行就由谁创建,
后运行的进程如果发现它想用的那个消息队列已经创建好了,就直接使用,当众多进程共享操作同一个
消息队列时,即可实现进程间的通信。

5.2.3 消息队列的函数

	所有system V ipc的API都是相似的,如果你能把消息队列的API搞清楚,后面的共享内存和信号量
API,理解起来很容易。
5.2.3.1 msgget函数
5.2.3.1.1 函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);

(a)功能:利用key值创建、或者获取一个消息队列。
	· 如果key没有对应任何消息队列,那就创建一个新的消息队列
		
	· 如果key已经对应了某个消息队列,说明你要的消息队列已经存在了,那就获取这个消息队
		列来使用
	
	
		估计你也感觉到了,key值也能够唯一的标识消息队列,那key值到底是个啥?
		后面再介绍。

(b)返回值
	· 成功:返回消息队列标识符(消息队列的ID)
			对于每一个创建好的消息队列来说,ID是固定的。
	
	· 失败:失败返回-1,并设置errno。
	
(c)参数
		
	int msgget(key_t key, int msgflg);

	·key值
	用于为消息队列生成(计算出)唯一的消息队列ID。
	
	我们可以指定三种形式的key值:
	
	- 第一种:指定为IPC_PRIVATE宏,指定这个宏后,每次调用msgget时都会创建一个新的消息队列。
			如果你每次使用的必须是新消息队列的话,就可以指定这个,不过这个用的很少。
			因为一般来说,只要有一个消息队列可以用来通信就可以了,并不需要每次都创建一个全新
		的消息队列。
			
	- 第二种:可以自己指定一个整形数,但是容易重复指定
			本来我想创建一个新的消息队列,结果我所指定的这个整形数,之前就已经被用于创建
		某个消息队列了,当我的指定重复时,msgget就不会创建新消息队列,而是使用的是别人
		之前就创建好的消息队列。
				
			所以我们也不会使用这种方式来指定key值。
			
			
	-	第三种:使用ftok函数来生成key
		 #include <sys/types.h>
		 #include <sys/ipc.h>

		 key_t ftok(const char *pathname, int proj_id);

			ftok通过指定路径名和一个整形数,就可以计算并返回一个唯一对应的key值,
		只要路径名和整形数不变,所对应的key值就唯一不变的。
		
		
			不过由于ftok只会使用整形数(proj_id)的低8位,因此我们往往会指定为一个ASCII码值,
		因为ASCII码值刚好是8位的整形数。
					
	
	int msgget(key_t key, int msgflg);
	· msgflg
		指定创建时的原始权限,比如0664
		
		创建一个新的消息队列时,除了原始权限,还需要指定IPC_CREAT选项。
	
		msgid = msgget(key, 0664|IPC_CREAT);
		
			如果key值没有对应任何消息队列,就会创建一个新的消息队列,此时就会用到msgflg参数,
		但是如果key已经对应了某个早已存在消息队列,就直接返回这个已存在消息队列的ID(标识符),
		此时不会用到msgflg参数。
			
			
			
(d)多个进程是如何共享到同一个消息队列的	
	1)创建进程
		如果创建者使用"./file", 'a'生成一个key值,然后调用msgget创建了一个消息队列,比如:
		key = ftok("./file", 'a');
		msgid = msgget(key, 0664|IPC_CREAT);
		
		当创建者得到msgid后,即可操作消息队列。
		
		
	2)其它共享操作消息队列的进程
	
			共享的方法很简单,只要你能拿到别人创建好的消息队列的ID,即可共享操作同一个消
		息队列,实现进程间通信。
	
	
			获取别人创建好的消息队列的ID,有两个方法:
		
		(a)创建者把ID保存到某文件,共享进程读出ID即可
				这种情况下,共享进程根本不需要调用msgget函数来返回ID。
		
			
		(b)调用msgget获取已在消息队列的ID
		
				· 使用ftok函数,利用与创建者相同的“路径名”和8位整形数,生成相同的key值
				
				· 调用msgget函数,利用key找到别人创建好的消息队列,返回ID
					
					key = ftok("./file", 'a');
					msgid = msgget(key, 0664|IPC_CREAT);
		
					拿到了消息队列的ID后就能共享操作了。
				
				这种方法是最常用的方法,因为ftok所用到的“路径名”和“8位的整形数”比较好记忆,
			所以,你只要记住别人生成key值时所用的“路径名”和“8位的整形数”,你就一定能共享
			操作别人创建好的消息队列。
5.2.3.1.2 代码演示
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <signal.h>

#define MSG_FILE "./msgfile"

#define MSG_SIZE 1024

struct msgbuf
{
	long mtype;         /* 放消息编号,必须 > 0 */
	char mtext[MSG_SIZE];  /* 消息内容(消息正文) */
};									


void print_err(char *estr)
{
	perror(estr);
	exit(-1);
}

int creat_or_get_msgque(void)
{
	int msgid = -1;
	key_t key = -1;
	int fd = 0;

	/* 创建一个消息队列的专用文件,ftok会用到这个文件的路径名 */
	fd = open(MSG_FILE, O_RDWR|O_CREAT, 0664);
	if(fd == -1) print_err("open fail");
	
	/* 利用存在的文件路径名和8位整形数,计算出key */
	key = ftok(MSG_FILE, 'a');
	if(key == -1) print_err("ftok fail");

	/* 利用key创建、或者获取消息队列 */
	msgid = msgget(key, 0664|IPC_CREAT);
	if(msgid == -1) print_err("msgget fail");

	return msgid;
}	

int msgid = -1;
//用于退出后删除消息队列
void signal_fun(int signo)
{
	msgctl(msgid, IPC_RMID, NULL);
	remove(MSG_FILE);	
	
	exit(-1);
}

int main(int argc, char **argv)
{	
	int ret = -1;
	long recv_msgtype = 0;
	
	if(argc !=  2)
	{
		printf("./a.out recv_msgtype\n");
		exit(-1);
	}
	recv_msgtype = atol(argv[1]);
	
	
	msgid = creat_or_get_msgque();

	ret = fork();
	if(ret > 0) //发送消息
	{
		signal(SIGINT, signal_fun);
		struct msgbuf msg_buf = {0};
		while(1)
		{
			bzero(&msg_buf, sizeof(msg_buf));
			/* 封装消息包 */
			scanf("%s", msg_buf.mtext);
			printf("input snd_msgtype:\n");
			scanf("%ld", &msg_buf.mtype);

			/* 发送消息包 */
			msgsnd(msgid, &msg_buf, MSG_SIZE, 0);	
		}
	}
	else if(ret == 0)//接收消息
	{
		struct msgbuf msg_buf = {0};
		int ret = 0;
		while(1)
		{
			bzero(&msg_buf, sizeof(msg_buf));
			ret = msgrcv(msgid, &msg_buf, MSG_SIZE, recv_msgtype, 0);
			if(ret > 0) 
			{
				printf("%s\n", msg_buf.mtext);
			}	
		}
	}
	
		
	return 0;
}
(a)如何验证消息队列是否被创建成功?
				
		使用ipcs命令即可查看,可跟接的选项有:
			- a 或者 什么都不跟:消息队列、共享内存、信号量的信息都会显示出来
			
			- m:只显示共享内存的信息    
			
			- q:只显示消息队列的信息
			
			- s:只显示信号量的信息
													
													
(b)system v ipc的缺点

			进程结束时,system v ipc不会自动删除,进程结束后,使用ipcs依然能够查看到。
		
			如何删除?	
			· 方法1:重启OS,很麻烦
			
			· 方法2:进程结束时,调用相应的API来删除,后面再讲
			
			· 方法3:使用ipcrm命令删除
			
				- 删除共享内存
					+ M:按照key值删除
						ipcrm -M key
						
					+ m:按照标识符删除
						ipcrm -m msgid
				
				- 删除消息队列
					+ Q:按照key值删除
					+ q:按照标识符删除
					
					
				- 删除信号量
					+ S:按照key值删除
					+ s:按照标识符删除
5.2.3.2 msgsnd函数
函数原型
	#include <sys/types.h>
	#include <sys/ipc.h>
	#include <sys/msg.h>
	
	int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
					
(a)功能:发送消息到消息队列上。
			说白了就是将消息挂到消息队列上。

(b)返回值
		· 成功:返回0,
		· 失败:返回-1,errno被设置

(c)参数
		· msqid:消息队列的标识符。
		· msgp:存放消息的缓存的地址,类型struct msgbuf类型
				这个缓存就是一个消息包(存放消息的结构体变量)。
				
			struct msgbuf
			{
						long mtype;         /* 放消息编号,必须 > 0 */
						char mtext[msgsz];  /* 消息内容(消息正文) */
			};				
			
		· msgsz:消息正文大大小。
					
		· msgflg:
			- 0:阻塞发送消息
				也就是说,如果没有发送成功的话,该函数会一直阻塞等,直到发送成功为止。
					
					
			- IPC_NOWAIT:非阻塞方式发送消息,不管发送成功与否,函数都将返回
					也就是说,发送不成功的的话,函数不会阻塞。
5.2.3.3 msgrcv函数
函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);						

(a)功能:接收消息
			说白了就是从消息队列中取出别人所放的某个编号的消息。

			
(b)返回值
		成功:返回消息正文的字节数
		失败:返回-1,errno被设置
		
		
(c)参数	
		· msqid:消息队列的标识符。
		· msgp:缓存地址,缓存用于存放所接收的消息
			
			类型还是struct msgbuf:
			struct msgbuf
			{
						long mtype;         /* 存放消息编号*/
						char mtext[msgsz];  /*存放 消息正文内容 */
			};					
			
			
		· msgsz:消息正文的大小
			
		· msgtyp:你要接收消息的编号
			
		· int msgflg:
			- 0:阻塞接收消息
				也就是说如果没有消息时,接收回阻塞(休眠)。
			
			- IPC_NOWAIT:非阻塞接收消息
				也就是说没有消息时,该函数不阻塞
5.2.3.4 进程结束时,自动删除消息队列
我们需要调用msgctl函数来实现。
msgctl函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
		
(a)功能
		ctl就是控制contrl的意思,从这个名字我们就能猜出,这个函数的功能是根据cmd指定的要求,
	去控制消息队列,比如进行哪些控制呢?
	
	· 获取消息队列的属性信息
	· 修改消息队列的属性信息
	· 删除消息队列
	· 等等
	
		我们调用msgctl函数的最常见目的就是删除消息队列,事实上,删除消息队列只是各种消息队列
	控制中的一种。
	
	
(b)参数
	int msgctl(int msqid, int cmd, struct msqid_ds *buf);

·	msqid:消息队列标识符
	
·	cmd:控制选项,其实cmd有很多选项,我这里只简单介绍三个
	
		- IPC_STAT:将msqid消息队列的属性信息,读到第三个参数所指定的缓存。
			
		- IPC_SET:使用第三个参数中的新设置去修改消息队列的属性
				+ 定一个struct msqid_ds buf。
				+ 将新的属性信息设置到buf中
				+ cmd指定为IPC_SET后,msgctl函数就会使用buf中的新属性去修改消息队列原有的属性。
			
			
		- IPC_RMID:删除消息队列
				删除消息队列时,用不到第三个参数,用不到时设置为NULL。
		
		- ... :略
		
	
·	buf:存放属性信息

	有的时候需要给第三个参数,有时不需要,取决于cmd的设置。
	buf的类型为struct msqid_ds,有关这个结构体类型,这里这里只进行简单了解。
	
	
	结构体中的成员都是用来存放消息队列的属性信息的。
struct msqid_ds 
{
		struct ipc_perm  msg_perm; /* 消息队列的读写权限和所有者 */
		time_t  msg_stime;    /* 最后一次向队列发送消息的时间*/
		time_t  msg_rtime;    /* 最后一次从消息队列接收消息的时间 */
		time_t  msg_ctime;    /* 消息队列属性最后一次被修改的时间 */
		unsigned  long __msg_cbytes; /* 队列中当前所有消息总的字节数 */
		msgqnum_t  msg_qnum;     /* 队列中当前消息的条数*/
		msglen_t msg_qbytes;  /* 队列中允许的最大的总的字节数 */
		pid_t  msg_lspid;     /* 最后一次向队列发送消息的进程PID */
		pid_t  msg_lrpid;     /* 最后一次从队列接受消息的进程PID */
};
		
struct ipc_perm 
{
	key_t          __key;       /* Key supplied to msgget(2):消息队列的key值 */
	uid_t          uid;         /* UID of owner :当前这一刻正在使用消息队列的用户 */
	gid_t          gid;         /* GID of owner :正在使用的用户所在用户组 */
	uid_t          cuid;        /* UID of creator :创建消息队列的用户 */
	gid_t          cgid;        /* GID of creator :创建消息队列的用户所在用户组*/
	unsigned short mode;        /* Permissions:读写权限(比如0664) */
	unsigned short __seq;       /* Sequence number :序列号,保障消息队列ID不被立即
																	重复使用 */
};

5.2.4 什么时候合适使用消息队列

	实际上消息队列这种通信方式,使用起来还是蛮方便的,因为不管是两个进程之间的通信,还是n多个进
程的网状交叉通信,消息队列都能搞定,完全可以替代前面讲的管道,

	特别是当你的程序必须涉及到多进程网状交叉通信时,消息队列是上上之选。

5.2.5 消息队列的缺点

	与管道一样,不能实现大规模数据的通信,大规模数据的通信,必须使用后面讲的“共享内存”来实现。

5.3 共享内存

	共享内存的API与消息队列的API非常相似,应该System V IPC的API都是差不多的,所以只要大家把
前面的消息队列拎清楚了,大家学习本小节的共享内存和之后的信号量时,你会觉非常的容易。
	共享内存就是OS在物理内存中开辟一大段缓存空间,不过与管道、消息队列调用read、write、
msgsnd、msgrcv等API来读写所不同的是,使用共享内存通信时,进程是直接使用地址来共享读写的。
	当然不管使用那种方式,只要能够共享操作同一段缓存,就都可以实现进程间的通信。
			
			
	不过如果直接使用地址来读写缓存时,效率会更高,但是如果是调用API来读写的话,中间必须经过重重
的OS函数调用之后,直到调用到最后一个函数时,该函数才会通过地址去读写共享的缓存,中间的调用过程
会降低效率。
	
	对于小数据量的通信来说,使用管道和消息队列这种使用API读写的通信方式很合适,但是如果进程涉及到
超大量的数据通信时,必须使用“共享内存”这种直接使用地址操作的通信方式,如果使用API来读写的话,
效率会非常的低。

5.3.1 共享内存的原理

	共享内存的实现原理很简单,进程空间不是没有交集吗,让他们的空间有交集不就行了吗。
		
	以两个进程使用共享内存来通信为例,实现的方法就是:
(1)调用API,让OS在物理内存上开辟出一大段缓存空间。
(2)让各自进程空间与开辟出的缓存空间建立映射关系
		
	就让虚拟地址和物理内存的实际物理地址建立一对一的对应关系,使用虚拟地址读写缓存时,虚拟
地址最终是要转为物理地址的,转换时就必须参考这个映射关系。

	总之建立映射关系后,每个进程都可以通过映射后的虚拟地址来共享操作实现通信了。

图:
在这里插入图片描述

	多个进程能不能映射到同一片空间,然后数据共享呢?
	答:当然是可以的
	
	不过当多个进程映射并共享同一个空间时,在写数据的时候可能会出现相互干扰,
		比如A进程的数据刚写了一半没写完,结果切换到B进程后,B进程又开始写,A的数据就被中间B的
	数据给岔开了这时往往需要加保护措施,让每个进程在没有操作时不要被别人干扰,等操作完以后,
	别的进程才能写数据。比如可以使用信号或者信号量来解决这种同步的问题。

5.3.2 共享内存的使用步骤

(1)进程调用shmget函数创建新的或获取已有共享内存
			shm是share memory的缩写。

(2)进程调用shmat函数,将物理内存映射到自己的进程空间
			说白了就是让虚拟地址和真实物理地址建议一一对应的映射关系。
			
			建立映射后,就可以直接使用虚拟地址来读写共享的内存空间了。
			

(3)shmdt函数,取消映射
		
(4)调用shmctl函数释放开辟的那片物理内存空间
			和消息队列的msgctl的功能是一样的,只不过这个是共享内存的。

		多个进程使用共享内存通信时,创建者只需要一个,同样的,一般都是谁先运行谁创建,其它后运行的
	进程发现已经被创建好了,就直接获取共享使用,大家共享操作同一个内存,即可实现通信。

5.3.2 共享内存的函数

5.3.2.1 shmget函数
5.3.2.1.1 函数原型
#include <sys/ipc.h>
#include <sys/shm.h>

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

1)功能:创建新的,或者获取已有的共享内				

	·	如果key值没有对应任何共享内存
			创建一个新的共享内存,创建的过程其实就是os在物理内存上划出(开辟出)一段物理内存
		空间出来。
		
	·	如果key值有对应某一个共享内存
			说明之前有进程调用msgget函数,使用该key去创建了某个共享内存,既然别人之前就创
		建好了,那就直接获取key所对应的共享内存。
	

2)返回值
	(a)成功:返回共享内存的标识符,以后续操作
	(b)失败:返回-1,并且errno被设置。
		
		
		int shmget(key_t key, size_t size, int shmflg);
		
3)参数
	(a)key:用于生成共享内存的标识符
			可以有三种设置:
			· IPC_PRIVATE:指定这个后,每次调用shmget时都会创建一个新共享内存。
			· 自己指定一个长整型数
			· 使用ftok函数,通过路径名和一个8位的整形数来生成key值

				
	(b)size:指定共享内存的大小,我们一般要求size是虚拟页大小的整数倍
			一般来说虚拟页大小是4k(4096字节),如果你指定的大小不是虚拟页的整数倍,也会自动
		帮你补成整数倍。
			
	(c)semflg:与消息队列一样
				指定原始权限和IPC_CREAT,比如0664|IPC_CREAT。
				
				只有在创建一个新的共享内存时才会用到,否者不会用到。
5.3.2.1.2 代码演示

共享内存写数据代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <errno.h>



#define SHM_FILE "./shmfile"

#define SHM_SIZE 4096

int shmid = -1;
void *shmaddr = NULL;	



void print_err(char *estr)
{
	perror(estr);	
	exit(-1);
}

void create_or_get_shm(void)
{
	int fd = 0;
	key_t key = -1;	

	fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
	if(fd == -1) print_err("open fail");
	
	key = ftok(SHM_FILE, 'b');
	if(key == -1) print_err("ftok fail");
	
	shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
	if(shmid == -1) print_err("shmget fail");

	//write(fd, &shmid, sizeof(shmid));
}

char buf[300] = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222\
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2222222222"};

void signal_fun(int signo)
{
	shmdt(shmaddr);
	shmctl(shmid, IPC_RMID, NULL);
	remove("./fifo");
	remove(SHM_FILE);
	
	exit(-1);	
}

int get_peer_PID(void)
{
	int ret = -1;
	int fifofd = -1;

	/* 创建有名管道文件 */
        ret = mkfifo("./fifo", 0664); 
        if(ret == -1 && errno != EEXIST) print_err("mkfifo fail");
	
	/* 以只读方式打开管道 */
	fifofd = open("./fifo", O_RDONLY);
        if(fifofd == -1) print_err("open fifo fail");

	/* 读管道,获取“读共享内存进程”的PID */
        int peer_pid;
        ret = read(fifofd, &peer_pid, sizeof(peer_pid));
        if(ret == -1) print_err("read fifo fail");

	return peer_pid; 
}

int main(void)
{
	int peer_pid = -1;

	/* 给SIGINT信号注册捕获函数,用于删除共享内存、管道、文件等 */
	signal(SIGINT, signal_fun);


	/* 使用有名管道获取读共享内存进程的PID */
	peer_pid = get_peer_PID();
	
	
	/* 创建、或者获取共享内存 */
	create_or_get_shm();
	
	//建立映射
	shmaddr = shmat(shmid, NULL, 0);
	if(shmaddr == (void *)-1) print_err("shmat fail");	
	
	while(1)
	{
		memcpy(shmaddr, buf, sizeof(buf));
		//保证写完后再读数据,当共享内存没有数据时,读进程休眠,当写进程把数据写完后,
		//将读进程唤醒。
		kill(peer_pid, SIGUSR1);
		sleep(1);
		
	}
	
	return 0;
}

共享内存读数据代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <errno.h>


#define SHM_FILE "./shmfile"

#define SHM_SIZE 4096

int shmid = -1;
void *shmaddr = NULL;	



void print_err(char *estr)
{
	perror(estr);	
	exit(-1);
}

void create_or_get_shm(void)
{
	int fd = 0;
	key_t key = -1;	

	fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
	if(fd == -1) print_err("open fail");
	
	key = ftok(SHM_FILE, 'b');
	if(key == -1) print_err("ftok fail");
	
	shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
	if(shmid == -1) print_err("shmget fail");
	
	//read(fd, &shmid, sizeof(shmid));
}

void signal_fun(int signo)
{
	if(SIGINT == signo)
	{
		shmdt(shmaddr);
		shmctl(shmid, IPC_RMID, NULL);
		remove("./fifo");
		remove(SHM_FILE);

		exit(-1);
	}
	else if(SIGUSR1 == signo)
	{
		
	}
}

void snd_self_PID(void)
{
	int ret = -1;
	int fifofd = -1;

	/* 创建有名管道文件 */
	mkfifo("./fifo", 0664); 
	if(ret == -1 && errno != EEXIST) print_err("mkfifo fail");
	
	/* 以只写方式打开文件 */
	fifofd = open("./fifo", O_WRONLY);
	if(fifofd == -1) print_err("open fifo fail");
	
	/* 获取当前进程的PID, 使用有名管道发送给写共享内存的进程 */
	int pid = getpid();
	ret = write(fifofd, &pid, sizeof(pid));//发送PID
	if(ret == -1) print_err("write fifo fail");
}

int main(void)
{

	/*给SIGUSR1注册一个空捕获函数,用于唤醒pause()函数 */
	signal(SIGUSR1, signal_fun);
	signal(SIGINT, signal_fun);

	/* 使用有名管道,讲当前进程的PID发送给写共享内存的进程 */
	snd_self_PID();	


	/* 创建、或者获取共享内存 */
	create_or_get_shm();


	//建立映射
	shmaddr = shmat(shmid, NULL, 0);
	if(shmaddr == (void *)-1) print_err("shmat fail");	
	
	while(1)
	{
		//无数据休眠,有数据唤醒
		pause();
		printf("%s\n", (char *)shmaddr);
		bzero(shmaddr, SHM_SIZE);
	}
	
	return 0;
}

说明:

(a)使用ipcs命令即可查看创建的共享内存:
	- a 或者 什么都不跟:消息队列、共享内存、信号量的信息都会显示出来
	
	- m:只显示共享内存的信息    
	
	- q:只显示消息队列的信息
	
	- s:只显示信号量的信息
												
												
(b)共享内存的删除

	进程结束时,system v ipc不会自动删除,进程结束后,使用ipcs依然能够查看到。

	如何删除?	
	· 方法1:重启OS,很麻烦
	
	· 方法2:进程结束时,调用相应的API来删除,后面再讲
	
	· 方法3:使用ipcrm命令删除
	
		- 删除共享内存
			+ M:按照key值删除
				ipcrm -M key
				
			+ m:按照标识符删除
				ipcrm -m msgid
		
		- 删除消息队列
			+ Q:按照key值删除
			+ q:按照标识符删除
			
			
		- 删除信号量
			+ S:按照key值删除
			+ s:按照标识符删除
5.3.2.2 shmat
(1)函数原型
#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);

1)功能
		将shmid所指向的共享内存空间映射到进程空间(虚拟内存空间),并返回影射后的起始
	地址(虚拟地址)。
		
		有了这个地址后,就可以通过这个地址对共享内存进行读写操作。
		
2)参数
	(a)shmid:共享内存标识符。
	
	(b)shmaddr:指定映射的起始地址
				有两种设置方式
	
			· 自己指定映射的起始地址(虚拟地址)。
				我们一般不会这么做,因为我们自己都搞不清哪些虚拟地址被用了,哪些没被用。
				
			· NULL:表示由内核自己来选择映射的起始地址(虚拟地址)。
					这是最常见的方式,也是最合理的方式,因为只有内核自己才知道哪些虚拟地址
				可用,哪些不可用。


		void *shmat(int shmid, const void *shmaddr, int shmflg);
	(c)shmflg:指定映射条件。
			
			· 0:以可读可写的方式映射共享内存
					也就是说映射后,可以读、也可以写共享内存。
			
			· SHM_RDONLY:以只读方式映射共享内存
					也就是说映射后,只能读共享内存,不能写。

					
					
3)返回值
	(a)成功:则返回映射地址
	(b)失败:返回(void *)-1,并且errno被设置。
5.3.2.3 shmdt函数
(1)函数原型
	#include <sys/types.h>
	#include <sys/shm.h>
	
	int shmdt(const void *shmaddr);	

1)功能:取消建立的映射。

2)返回值:调用成功返回0,失败返回-1,且errno被设置。
	
	
3)参数
	shmaddr:映射的起始地址(虚拟地址)。
5.3.2.4 shmctl函数
(1)函数原型
#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

(a)功能:根据cmd的要求,对共享内存进行相应控制。
				比如:
			· 获取共享内存的属性信息
			· 修改共享内存的属性信息
			· 删除共享内存
			· 等等
			
				删除共享内存是最常见的控制。
		
(b)参数
· shmid:标识符。
· cmd:控制选项

	- IPC_STAT:从内核获取共享内存属性信息到第三个参数(应用缓存)。
			
	- IPC_SET:修改共享内存的属性。
			修改方法与消息队列相同。
	
	
	- IPC_RMID:删除共享内存,不过前提是只有当所有的映射取消后,才能删除共享内存。
			删除时,用不着第三个参数,所以设置为NULL
			
	
· buf
	buf的类型为struct shmid_ds。
	
- cmd为IPC_STAT时
	buf用于存储原有的共享内存属性,以供查看。
		
- cmd为IPC_SET时
	buf中放的是新的属性设置,用于修改共享内存的属性。
	

- struct shmid_ds结构体

struct shmid_ds 
{
	struct ipc_perm shm_perm;    /* Ownership and permissions:权限 */
	size_t shm_segsz;   /* Size of segment (bytes):共享内存大小 */
	time_t shm_atime;   /* Last attach time:最后一次映射的时间 */
	time_t shm_dtime;   /* Last detach time:最后一次取消映射的时间 */
	time_t shm_ctime;   /* Last change time:最后一次修改属性信息的时间 */
	pid_t shm_cpid;    /* PID of creator:创建进程的PID */
	pid_t shm_lpid;    /* PID of last shmat(2)/shmdt(2) :当前正在使用进程的PID*/
	shmatt_t shm_nattch;  /* No. of current attaches:映射数量,
												 * 标记有多少个进程空间映射到了共享内存上
												 * 每增加一个映射就+1,每取消一个映射就-1 */ 
	...
};

struct ipc_perm,这个结构体我们在讲消息队列时已经讲过,这里不再重复讲。
struct ipc_perm 
{
	 key_t          __key;    /* Key supplied to shmget(2) */
	 uid_t          uid;      /* UID of owner */
	 gid_t          gid;      /* GID of owner */
	 uid_t          cuid;     /* UID of creator */
	 gid_t          cgid;     /* GID of creator */
	 unsigned short mode;     /* Permissions + SHM_DEST andSHM_LOCKED flags */
	 unsigned short __seq;    /* Sequence number */
};
								
	4)返回值
			调用成功0,失败则返回-1,并且errno被设置。

5.4 system V IPC 之 信号量(或信号灯)semaphore

5.4.1 信号量的作用

当多个进程/线程进行共享操作时,用于资源保护,以防止出现相互干扰的情况。

再间简洁一点,信号量用于“资源的保护“。

(1)进程信号量
		实现的是进程所操作资源的保护。

(2)线程信号量
		实现的是线程所操作资源的保护。
5.4.1.1 什么是进程资源保护
我们讲的是进程的资源保护,实际上线程的资源保护也是类似的原理。
			
为了更直观的讲解,我们直接通过例子来介绍什么是“进程资源保护”。


(1)例子1:多进程操作共享内存
			比如,多个进程同时向共享内存里面写数据时,可能会出现数据相互干扰的情况。
		
			比如,某个进程写数据操作还没有写完时,进程的时间片就到了,然后被切换到另一个写
		“共享内存”的进程上运行,这个进程会接着往共享内存里面写数据,此时显然就把第一个进程写
		的数据给隔断,这就形成了数据相互干扰。

			如果只是普通数据的话无所谓,但是如果是很重要的数据的话,这种干扰是无法接受。
		

(2)例子2:多进程操作文件
		比如当多个进程同时共享向文件里面写数据时,同样会出现和共享写“共享内存”相同的情况
	

为了避免出现以上所说的相互干扰的问题,就需要加入资源保护的措施,保护的目的就是,保证每个
进程在没有把数据读、写完整之前,其它进程不能进行读、写操作,以防止干扰别人。

疑问:资源保护,这个“资源”到底指的是谁?
答:这个资源指的就是你操作的数据,保护的目的就是不要出现相互干扰,导致紊乱和错误数据的产生。
5.4.1.2 资源保护操作的种类
资源保护的操作分两种,一种叫互斥,另一个种叫同步。
		
(1)互斥
		对于互斥操作来说,多进程共享操作时,多个进程间不关心谁先操作、谁后操作的先后顺序问题,
	它们只关心一件事,那就是我在操作时别人不能操作。
	
		就算当前正在操作的进程它的时间片到了,切换到了其它进程上,但是当该进程检测到上一个进程
	还没有操作完时,该进程在当前的时间片内会休眠,直到再次切换会上一个进程,将操作完成后再切
	换回来,此时才能进行操作。
			
		这跟上厕所时把门关起来是一样的,我在蹲坑时你不能蹲,你在蹲坑时我不能蹲,这就是互斥,至于
	蹲坑先后顺序并没有要求。


(2)同步
		同步其实本身就包含了互斥,不过同步不仅仅只互斥,同步对于谁先操作、谁后操作的先后顺序有
	要求,比如规定A进程先写,然后是B进程写,然后是C进程写,绝对不能出现这操作顺序以外的顺序。
	
		所以所谓同步就是,多个共享操作时,进程必须要有统一操作的步调,按照一定的顺序来操作。	
	
	疑问:同步有意义吗?
	答:我们讲共享内存时,例子代码不就需要同步吗。
			
			
	
(3)实现同步、互斥,其实就是加锁
	这个很形象,我要操作我就上把锁,我上锁的过程中你就不能操作,直到我把锁打开了,你才能操作,
你操作时也会加锁,加锁后我就不能操作了。
	
	所以说信号量就是一个加锁机制,通过加锁来实现同步和互斥。
	
	说到加锁,我们讲到后面“高级IO”时,我们还会讲到“文件锁”这个东西,顾名思义,文件锁就是专门用
来给文件时上锁的,讲到时在详细介绍。

	其实,不管是进程还是线程,都存在同步和互斥的问题,同步和互斥的目的其实就是为了实现“资源”的
保护,不要让数据(资源)出现紊乱。
	

(4)疑问:信号量既然是一种加锁机制,为什么进程信号量会被归到了进程间通信里面呢?
	
		资源保护时,某个进程的操作没有完全完成之前,别人是不能操作的,那么进程间必须相互知道
	对方的操作状态,必须会涉及到通信过程。
			
	所以信号量实现资源保护的本质就是,通过通信让各个进程了解到操作状态,然后查看自己能不能操作。

5.4.2 使用信号量实现互斥

	进程信号量既能实现进程的互斥,也能实现进程的同步,不过有些“资源保护机制”就只能实现互斥,
而不能实现同步。
	虽然我们这里主要是讲互斥,但是也会捎带的提到同步,为后面讲同步打基础。
5.4.2.1 需要互斥实现“资源保护”的例子

semaphore.h:

#ifndef H_SEM_H
#define H_SEM_H

extern void print_err(char *estr);
extern int creat_or_get_sem(int nsems);
extern void init_sem(int semid, int semnum, int val);
extern void del_sem(int semid, int nsems);
extern void p_sem(int semid, int semnum_buf[], int nsops);
extern void v_sem(int semid, int semnum_buf[], int nsops);

#endif

semaphore.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

union semun 
{
	int val;   
	struct semid_ds *buf;    
	unsigned short  *array;  /* 不做要求 */
	struct seminfo  *__buf;  /* 不做要求 */
};

#define SEM_FILE "./semfile"

void print_err(char *estr)
{
        perror(estr);
        //exit(-1);
}

int creat_or_get_sem(int nsems)
{
	int semid;
	int fd = -1;
	key_t key = -1;

	fd = open(SEM_FILE, O_RDWR|O_CREAT, 0664);
	if(fd == -1) print_err("open ./semfile fail");
	
	key = ftok(SEM_FILE, 'a');	
	if(key == -1) print_err("ftok fail");

	semid = semget(key, nsems, 0664|IPC_CREAT);
	if(semid == -1) print_err("semget fail");

	return semid;	
}

void init_sem(int semid, int semnum, int val)
{
	int ret = -1;
	union semun sem_un;

	/* semnum:信号量编号
	 * SETVAL:设置信号量初始值cmd
	 * sem_un:初始值
	 */	
	sem_un.val = val;
	ret = semctl(semid, semnum, SETVAL, sem_un);
	if(ret == -1) print_err("semctl fail");
}


void del_sem(int semid, int nsems)
{
	int ret = 0;
	int i = 0;

	for(i=0; i<nsems; i++)
	{	
		ret = semctl(semid, i, IPC_RMID);
		if(ret == -1) print_err("semctl del sem fail");
	}

	remove(SEM_FILE);
}

void p_sem(int semid, int semnum_buf[], int nsops)
{
	int i = 0;
	int ret = -1;
	struct sembuf sops[nsops];
	
	for(i=0; i<nsops; i++)
	{
		sops[i].sem_num = semnum_buf[i];//信号量编号
		sops[i].sem_op 	= -1;//-1 p操作
		sops[i].sem_flg = SEM_UNDO;//防止死锁 
	}

	ret = semop(semid, sops, nsops);
	if(ret == -1) print_err("semop p fail");
}


void v_sem(int semid, int semnum_buf[], int nsops)
{
	int i = 0;
	int ret = -1;
	struct sembuf sops[nsops];
	
	for(i=0; i<nsops; i++)
	{
		sops[i].sem_num = semnum_buf[i];//信号量编号
		sops[i].sem_op 	= 1;//+1 v操作
		sops[i].sem_flg = SEM_UNDO;//防止死锁 
	}

	ret = semop(semid, sops, nsops);
	if(ret == -1) print_err("semop p fail");
}

share_write_file.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include "semaphore.h"



#define NSEMS 1

int semid;


void signal_fun(int signo)
{
	del_sem(semid, NSEMS);
	exit(-1);
}

int main(void)
{
	int i = 0;
	int ret = 0;
	int fd = -1;
	int semnum_buf[1] = {0};
	
	fd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664);
	if(fd == -1) print_err("open file fail");

	semid = creat_or_get_sem(NSEMS);
	
	for(i=0; i<NSEMS; i++)
	{
		init_sem(semid, i, 1);
	}

	ret = fork();
	if(ret > 0)
	{
		signal(SIGINT, signal_fun);
		while(1)
		{
			semnum_buf[0] = 0;//设置要操作的信号量的编号
			p_sem(semid, semnum_buf, 1); //P操作
			write(fd, "hello ", 6);
			write(fd, "world\n", 6);
			semnum_buf[0] = 0; //设置要操作的信号量的编号
			v_sem(semid, semnum_buf, 1);//v操作
		}		
	}
	else if(ret == 0)
	{
		while(1)
		{
			semnum_buf[0] = 0;//设置要操作的信号量的编号
			p_sem(semid, semnum_buf, 1); //P操作
			write(fd, "hhhhh ", 6);
			write(fd, "wwwww\n", 6);
			semnum_buf[0] = 0; //设置要操作的信号量的编号
			v_sem(semid, semnum_buf, 1);//v操作
		}		
	}

	return 0;
}
5.4.2.2 进程信号量实现互斥的原理
(1)什么是进程信号量
					
	简单理解的话,信号量其实是OS创建的一个共享变量,进程在进行操作之前,会先检查这个变量的值,
这变量的值就是一个标记,通过这个标记就可以知道可不可以操作,以实现互斥。
		
		
(2)多值信号量和二值信号量

	1)二值信号量
			同步和互斥时使用的都是二值信号量。
			
			二值信号量的值就两个,0和1,0表示不可以操作,1表示可以操作。
		通过对变量进行0、1标记,就可以防止出现相互干扰情况。
			
	2)多值信号量
			信号量的最大值>1,比如为3的话,信号量允许的值为0、1、2、3。
			
			多值信号量用的不是很多,所这里只简单的提一下。
				
		
(3)信号量集合
			我们说信号量其实是一个OS创建的,供相关进程共享的int变量,只不过我们在调用相关API
		创建信号量时,我们创建的都是一个信号量集合,所谓集合就是可能会包含好多个信号量。
				
			用于互斥时,集合中只包含一个信号量。
			
			用于同步时,集合中会包含多个信号量,至于多少个,需要看情况。
			

(4)信号量的使用步骤

		1)进程调用semget函数创建新的信号量集合,或者获取已有的信号量集合。

		2)调用semctl函数给集合中的每个信号量设置初始值

		3)调用semop函数,对集合中的信号量进行pv操作
				什么是pv操作?
					pv操作其实说白了就是加锁、解锁操作。
					
				(a)P操作(加锁):对信号量的值进行-1,如果信号量的值为0,p操作就会阻塞
					
				(b)V操作(解锁):对信号量的值进行+1,V操作不存在阻塞的问题
				
				总之通过pv操作(加锁、解锁),就能够实现互斥,以防止出现干扰。
				
				正如我们前面总结的,加锁、解锁就跟上厕所蹲坑把门栓起来,完事了再把门打开是
			一样的,上厕所时通过门栓的加锁和解锁,就实现了上厕所蹲坑的互斥,防止上厕所的相互
			干扰。
				
		
		4)调用semctl删除信号量集合

5.4.3 信号量相关的API

5.4.3.1 semget函数
1)函数原型
	#include <sys/types.h>
	#include <sys/ipc.h>
	#include <sys/sem.h>
	
	int semget(key_t key, int nsems, int semflg);
		
	sem就是semaphore的缩写。

(a)功能:根据key值创建新的、或者获取已有的信号量集合,并返回其标识符。
		· 实现互斥时:集合中只需要一个信号量
		· 实现同步时:集合中需要多个信号量	
				
(b)参数
		·  key:设置同消息队列和共享内存。
			一般都使用ftok获取key值。
			
		· nsems:指定集合中信号量的个数。
			用于互斥时,数量都指定为1,因为只需要一个信号量。
			
			如果是同步的话就需要至多为多个,至于到底是多少个,讲到同步时再说。
		
		· semflg:设置同消息队列和共享内存。
			一般都设置为0664|IPC_CREAT。
		
(c)返回值:调用成功则返回信号量集合的标识符,失败则返回-1,并且errno被设置。
5.4.3.2 semctl函数
1)函数原型
	#include <sys/types.h>
	#include <sys/ipc.h>
	#include <sys/sem.h>
	
	int semctl(int semid, int semnum, int cmd, ...);
	
(a)功能
			根据cmd的要求对集合中的各个信号量进行控制,...表示它是一个变参函数,如果第四
		个参数用不到的话,可以省略不写。
			

(b)返回值:调用成功返回非-1值,失败则返回-1,errno被设置。


(c)参数说明
· semid:信号量标识符。
	通过标识符就能找到信号量集合。
		
· semnum:集合中某个信号量的编号。
	信号量的编号为非负整数,而且是自动从0开始编号的。
	
	通过信号量编号就能找到集合中对应信号量,然后对这个具体的信号量进行控制操作。
		

int semctl(int semid, int semnum, int cmd, ...);
· cmd:控制选项。

	- IPC_STAT:将信号量的属性信息从内核读到第四个参数所以指定的
						struct semid_ds缓存中。
						
						
	- IPC_SET:修改属性信息,此时也会用到struct semid_ds结构体变量
			具体的修改方法同消息队列和共享内存。

			
	- IPC_RMID:删除信号量集合时,并不需要把所有的信号量都删除掉后才能删除,只需要指定semid
			和IPC_RMID就可以不把整个信号量集合删除,其中第二个参数semnum没有被用到,所以
			semnum的值可以随便写,不过我们一般都是把它写为0。
			比如:semctl(semid, 0, IPC_RMID); 
	
	
	int semctl(int semid, int semnum, int cmd, ...);
	- SETVAL:通过第四个参数,给集合中semnu编号的信号量设置一个int初始值。
			在前面就说过,如果是二值信号量的话,设置初始值要么是0,要么是1,如果信号量的
		目的是互斥的话,基本都是设置为1。
		
			当设置为1后,多几个进程互斥操作时,那就是谁先运行就谁先操作。
				
			如果是同步的话,初值是1还是0,这要就要看具体的情况了。
			
	- 其它选项:省略
		
		其中信号量的IPC_STAT、IPC_SET、IPC_RMID与消息队列和共享内存的
	IPC_STAT、IPC_SET、IPC_RMID是一样的。
		但是SETVAL确属于进程信号量所独有的选项。
	
	对于信号量来说,IPC_RMID、SETVAL是最常用的两个选项。
		


int semctl(int semid, int semnum, int cmd, ...);
· ...

	...表示,如果用不到时可以省略不写。
	
	通过前面cmd的介绍我们可以看出,第四个参数具体设置为什么其实是不一定的,比如
	
	- cmd为IPC_STAT:第四个参数应为struct semid_ds类型的缓存。
					有关struct semid_ds结构体我们不再介绍,因为与共享内存的
					struct shmid_ds,以及消息队列的struct msqid_ds结构体是类似的。
									
								
	- cmd为SETVAL:第四个参数应该设置为一个int的值,用于初始化信号量。
	
		从以上可以看出,第四个参数对应内容是变着的,为了应对这种变化就用到了一个联合体。
	  union semun {
			 int              val;    
			 struct semid_ds *buf;    
			 unsigned short  *array;  /* 不做要求 */
			 struct seminfo  *__buf;  /* 不做要求 */
		};
			
			这个联合体类型并没有被定义在信号量相关的系统头文件中,我们使用这个联合体时,
		我们需要自己定义这个类型,至于联合体类型名可以自己定,不过一般都是直接沿用
		semun这个名字。
			
			成员:
			val:存放用于初始化信号量的值
			buf:存放struct semid_ds结构体变量的地址
		
			有关联合体的详细讲解,请看《C深度解析课》这门课,联合体的介绍属于基础
		课程的内容,所以我们这里不再赘述。
			
			
		疑问:这个联合怎么用?
		
		+ 例1:当需要指定struct semid_ds缓存时
			
			union semun sem_un; //定义一个联合体变量
			struct semid_ds buff; //定义一个struct semid_ds缓存
			
			sem_un.buf = &buff;  //现在整个联合体的值就是buf中缩放的buff的地址	
			semctl(semid, 0, IPC_STAT, sem_un); //这里将联合体传递给semctl函数,
						//其实就是将buff的地址传递给了semctl函数
			
			
		+ 例2:当需要指定信号量的int初始值时
			union semun sem_un; 
			sem_un.val = 1;  //现在整个联合体的值就是1	
			semctl(semid, 0, IPC_STAT, sem_un); 
5.4.3.3 semop函数
1)函数原型
	#include <sys/types.h>
	#include <sys/ipc.h>
	#include <sys/sem.h>

	int semop(int semid, struct sembuf *sops, unsigned nsops);

	op是operate操作的意思。

(a)功能:对指定的信号量进行p操作、或者是v操作。
	· p操作:将信号量的值-1
		当信号量的值为0时,p操作默认是阻塞的。
		
	
	· v操作:将信号量的值+1
		v操作不存在阻塞的问题。
	
			对于二值信号量来说,v操作后,值就从0变为了1,这就表示我操作完了,其它进程运行时
		就可以进行p操作了。

				
(b)返回值:调用成功返回0,失败则返回-1,errno被设置。


	int semop(int semid, struct sembuf *sops, unsigned nsops);
(c)参数
· semid:信号量集合的标识符。

· sops:这个参数更好理解的写法是struct sembuf sops[],
		第三个参数nsops就是用于指定数组元素个数的。
	
		每一个数组成员对应一个信号量,每一个元素都是一个struct sembuf结构体变量,内部成员的
	决定着:
		- 你要对集合中哪一个信号量进行操作
		- 要进行的是p操作呢,还是v操作
	
	- 结构体成员
		struct sembuf
		{
			unsigned short sem_num;  
			short          sem_op;
			short          sem_flg;  
		}
			这个结构体不需要我们自己定义,因为在semop的头文件中已经定义了。
			
			如果你无法判断这个结构体是否需要我们自己定义,那你就不要定义,如果编译提示这
		个结构体类型不存在,就说明需要自己定义,编译通过就说明在系统头文件中早就定义好了。
		
		+ sem_num:信号量编号,决定对集合中哪一个信号量进行pv操作
		+ sem_op:设置为-1,表示想-1进行p操作,设置1表示想+1进行v操作
		+ sem_flg:
			· IPC_NOWAIT: 
					一般情况下,当信号量的值为0时进行p操作的话,semop的p操作会阻塞。
					如果你不想阻塞的话,可以指定这个选项,NOWAIT就是不阻塞的意思。
						
						不过除非某些特殊情况,否则我们不需要设置为非阻塞。
										
			· SEM_UNDO:防止死锁
					还是以二值信号量为例,当进程在v操作之前就结束时,信号量的值就会一直保持
				为0,那么其它进程将永远无法p操作成功,会使得进程永远休眠下去,这造成就是死锁。
			
					但是设置了SEM_UNDO选项后,如果进程在结束时没有V操作的话,OS会自动帮忙
				V操作,防止死锁。

5.4.4 使用信号量实现同步

5.4.4.1 什么是同步
	让多个进程按照固定的步调做事,我们前面就说过,同步本身就是互斥的。
	
	实现同步时,同步的进程可以是亲缘进程,也可以是非亲缘进程。
5.4.4.2 同步举例1
通过同步让三个亲缘进程按照顺序打印出111111、222222、333333。

代码演示:
semaphore.h:

#ifndef H_SEM_H
#define H_SEM_H

extern void print_err(char *estr);
extern int creat_or_get_sem(int nsems);
extern void init_sem(int semid, int semnum, int val);
extern void del_sem(int semid, int nsems);
extern void p_sem(int semid, int semnum_buf[], int nsops);
extern void v_sem(int semid, int semnum_buf[], int nsops);

#endif

semaphore.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

union semun 
{
	int val;   
	struct semid_ds *buf;    
	unsigned short  *array;  /* 不做要求 */
	struct seminfo  *__buf;  /* 不做要求 */
};

#define SEM_FILE "./semfile"

void print_err(char *estr)
{
        perror(estr);
        //exit(-1);
}

int creat_or_get_sem(int nsems)
{
	int semid;
	int fd = -1;
	key_t key = -1;

	fd = open(SEM_FILE, O_RDWR|O_CREAT, 0664);
	if(fd == -1) print_err("open ./semfile fail");
	
	key = ftok(SEM_FILE, 'a');	
	if(key == -1) print_err("ftok fail");

	semid = semget(key, nsems, 0664|IPC_CREAT);
	if(semid == -1) print_err("semget fail");

	return semid;	
}

void init_sem(int semid, int semnum, int val)
{
	int ret = -1;
	union semun sem_un;

	/* semnum:信号量编号
	 * SETVAL:设置信号量初始值cmd
	 * sem_un:初始值
	 */	
	sem_un.val = val;
	ret = semctl(semid, semnum, SETVAL, sem_un);
	if(ret == -1) print_err("semctl fail");
}


void del_sem(int semid, int nsems)
{
	int ret = 0;

	ret = semctl(semid, 0, IPC_RMID);
	if(ret == -1) print_err("semctl del sem fail");

	remove(SEM_FILE);
}

void p_sem(int semid, int semnum_buf[], int nsops)
{
	int i = 0;
	int ret = -1;
	struct sembuf sops[nsops];
	
	for(i=0; i<nsops; i++)
	{
		sops[i].sem_num = semnum_buf[i];//信号量编号
		sops[i].sem_op 	= -1;//-1 p操作
		sops[i].sem_flg = SEM_UNDO;//防止死锁 
	}

	ret = semop(semid, sops, nsops);
	if(ret == -1) print_err("semop p fail");
}


void v_sem(int semid, int semnum_buf[], int nsops)
{
	int i = 0;
	int ret = -1;
	struct sembuf sops[nsops];
	
	for(i=0; i<nsops; i++)
	{
		sops[i].sem_num = semnum_buf[i];//信号量编号
		sops[i].sem_op 	= 1;//+1 v操作
		sops[i].sem_flg = SEM_UNDO;//防止死锁 
	}

	ret = semop(semid, sops, nsops);
	if(ret == -1) print_err("semop p fail");
}


sync.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include "semaphore.h"



#define NSEMS 3

int semid;


void signal_fun(int signo)
{
	del_sem(semid, NSEMS);
	exit(-1);
}

int main(void)
{
	int i = 0;
	int ret = 0;
	int fd = -1;
	int semnum_buf[1] = {0};
	
	//创建信号量集合
	semid = creat_or_get_sem(NSEMS);

	//初始化信号量集合中的每个信号量
	for(i=0; i<NSEMS; i++)
	{
		if(i == 0) init_sem(semid, i, 1);
		else init_sem(semid, i, 0);
	}

	ret = fork();
	if(ret > 0)
	{
		ret = fork();
		if(ret > 0) //父进程
		{
			while(1)
			{	
				semnum_buf[0] = 2;
				p_sem(semid, semnum_buf, 1);
				printf("333333\n");
				sleep(1);
				semnum_buf[0] = 0;
				v_sem(semid, semnum_buf, 1);
				
			}		
		}
		else if(ret == 0) //子进程2
  		{
			while(1)
			{
				semnum_buf[0] = 1;
				p_sem(semid, semnum_buf, 1);
				printf("222222\n");
				sleep(1);
				semnum_buf[0] = 2;
				v_sem(semid, semnum_buf, 1);
			}		
		}
	}
	else if(ret == 0)//子进程1
	{
		signal(SIGINT, signal_fun);
		while(1)
		{
			semnum_buf[0] = 0;
			p_sem(semid, semnum_buf, 1);
			printf("111111\n");
			sleep(1);
			semnum_buf[0] = 1;
			v_sem(semid, semnum_buf, 1);
		}		
	}
	
	return 0;
}




5.4.4.3 同步例子2
使用信号量来解决共享内存的同步问题

semaphore.h:

#ifndef H_SEM_H
#define H_SEM_H

extern int creat_or_get_sem(int nsems);
extern void init_sem(int semid, int semnum, int val);
extern void del_sem(int semid);
extern void p_sem(int semid, int semnum_buf[], int nsops);
extern void v_sem(int semid, int semnum_buf[], int nsops);

#endif



semaphore.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

union semun 
{
	int val;   
	struct semid_ds *buf;    
	unsigned short  *array;  /* 不做要求 */
	struct seminfo  *__buf;  /* 不做要求 */
};

#define SEM_FILE "./semfile"

static void print_err(char *estr)
{
        perror(estr);
        //exit(-1);
}

int creat_or_get_sem(int nsems)
{
	int semid;
	int fd = -1;
	key_t key = -1;

	fd = open(SEM_FILE, O_RDWR|O_CREAT, 0664);
	if(fd == -1) print_err("open ./semfile fail");
	
	key = ftok(SEM_FILE, 'a');	
	if(key == -1) print_err("ftok fail");

	semid = semget(key, nsems, 0664|IPC_CREAT);
	if(semid == -1) print_err("semget fail");

	return semid;	
}

void init_sem(int semid, int semnum, int val)
{
	int ret = -1;
	union semun sem_un;

	/* semnum:信号量编号
	 * SETVAL:设置信号量初始值cmd
	 * sem_un:初始值
	 */	
	sem_un.val = val;
	ret = semctl(semid, semnum, SETVAL, sem_un);
	if(ret == -1) print_err("semctl fail");
}


void del_sem(int semid)
{
	int ret = 0;

	ret = semctl(semid, 0, IPC_RMID);
	if(ret == -1) print_err("semctl del sem fail");

	remove(SEM_FILE);
}

void p_sem(int semid, int semnum_buf[], int nsops)
{
	int i = 0;
	int ret = -1;
	struct sembuf sops[nsops];
	
	for(i=0; i<nsops; i++)
	{
		sops[i].sem_num = semnum_buf[i];//信号量编号
		sops[i].sem_op 	= -1;//-1 p操作
		sops[i].sem_flg = SEM_UNDO;//防止死锁 
	}

	ret = semop(semid, sops, nsops);
	if(ret == -1) print_err("semop p fail");
}


void v_sem(int semid, int semnum_buf[], int nsops)
{
	int i = 0;
	int ret = -1;
	struct sembuf sops[nsops];
	
	for(i=0; i<nsops; i++)
	{
		sops[i].sem_num = semnum_buf[i];//信号量编号
		sops[i].sem_op 	= 1;//+1 v操作
		sops[i].sem_flg = SEM_UNDO;//防止死锁 
	}

	ret = semop(semid, sops, nsops);
	if(ret == -1) print_err("semop p fail");
}



shm1.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include "semaphore.h"



#define SHM_FILE "./shmfile"

#define SHM_SIZE 4096

int shmid = -1;
int semid = -1;
void *shmaddr = NULL;	



static void print_err(char *estr)
{
	perror(estr);	
	exit(-1);
}

void create_or_get_shm(void)
{
	int fd = 0;
	key_t key = -1;	

	fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
	if(fd == -1) print_err("open fail");
	
	key = ftok(SHM_FILE, 'b');
	if(key == -1) print_err("ftok fail");
	
	shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
	if(shmid == -1) print_err("shmget fail");

	//write(fd, &shmid, sizeof(shmid));
}

char buf[300] = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222\
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2222222222"};

void signal_fun(int signo)
{
	shmdt(shmaddr);
	shmctl(shmid, IPC_RMID, NULL);
	
	del_sem(semid);//删除信号量集合	

	remove("./fifo");
	remove(SHM_FILE);
	
	exit(-1);	
}



int main(void)
{
	int peer_pid = -1;

	/* 给SIGINT信号注册捕获函数,用于删除共享内存、管道、文件等 */
	signal(SIGINT, signal_fun);


	/* 创建、或者获取共享内存 */
	create_or_get_shm();
	
	//创建信号量集合	
	semid = creat_or_get_sem(2);

	/* 初始化信号量集合 */
	int i = 0;
	for(i=0; i<2; i++)
	{		
		//将编号0的信号量初始化为1,其它初始化为0
		if(i == 0) init_sem(semid, i, 1);
		else init_sem(semid, i, 0);
	}
	
	//建立映射
	shmaddr = shmat(shmid, NULL, 0);
	if(shmaddr == (void *)-1) print_err("shmat fail");	
	
	int semnum_buf[1] = {0};//存放信号量的编号
	while(1)
	{	
		//p sem 0
		semnum_buf[0] = 0;
		p_sem(semid, semnum_buf, 1);
		
		/* 向共享内存写数据 */
		memcpy(shmaddr, buf, sizeof(buf));
		sleep(1);
		
		//v sem 1
		semnum_buf[0] = 1;
		v_sem(semid, semnum_buf, 1);
	}
	
	return 0;
}


shm2.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include "semaphore.h"

#define SHM_FILE "./shmfile"

#define SHM_SIZE 4096

int shmid = -1;
int semid = -1;
void *shmaddr = NULL;	



void print_err(char *estr)
{
	perror(estr);	
	exit(-1);
}

void create_or_get_shm(void)
{
	int fd = 0;
	key_t key = -1;	

	fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
	if(fd == -1) print_err("open fail");
	
	key = ftok(SHM_FILE, 'b');
	if(key == -1) print_err("ftok fail");
	
	shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
	if(shmid == -1) print_err("shmget fail");
	
	//read(fd, &shmid, sizeof(shmid));
}

void signal_fun(int signo)
{
	if(SIGINT == signo)
	{
		shmdt(shmaddr);
		shmctl(shmid, IPC_RMID, NULL);
		remove("./fifo");
		remove(SHM_FILE);

		exit(-1);
	}
	else if(SIGUSR1 == signo)
	{
		
	}
}

int main(void)
{
	signal(SIGINT, signal_fun);

	/* 创建、或者获取共享内存 */
	create_or_get_shm();
	
	//获取别人创建号的信号量
	semid = creat_or_get_sem(2);

	//建立映射
	shmaddr = shmat(shmid, NULL, 0);
	if(shmaddr == (void *)-1) print_err("shmat fail");	

	int semnum_buf[1] = {0};//存放信号量编号	
	while(1)
	{
		//p sem 1
		semnum_buf[0] = 1;
                p_sem(semid, semnum_buf, 1);

		//从共享内存去除数据并打印显示
		printf("%s\n", (char *)shmaddr);
		bzero(shmaddr, SHM_SIZE);//清空共享内存
		
		//v sem 0
                semnum_buf[0] = 0;
                v_sem(semid, semnum_buf, 1);
	}
	
	return 0;
}




  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值