自学 Linux 12—进程间通信和同步—半双工管道、命名管道、消息队列、信号量、共享内存、信号

进程间通信和同步

  在 Linux 下的多个进程间的通信机制叫做进程间通信(IPC,InterProcess Communication),它是多个进程之间相互沟通的一种方法。在 Linux 下有多种进程间通信的方法:半双工管道FIFO命名管道)、消息队列信号量共享内存等。使用这些通信机制可以为 Linux 下的网络服务器开发提供灵活而又坚固的框架。

1. 半双工管道

  管道是一种把两个进程之间的标准输入和标准输出连接起来的机制。管道是一种历史悠久的进程间通信的办法,自 UNIX 操作系统诞生,管道就存在了。

1.基本概念

  由于管道仅仅是将某个进程的输出和另一个进程的输入相连接的单向通信的办法,因此称其为“ 半双工 ”。在 shell 中管道用 “” 表示,如下图(管道示意图)所示,是管道的一种使用方式。

管道示意图

$ls -l|grep *.c

  把 ls -l 的输出当做 “ grep *.c ” 的输入,管道在前一个进程中建立输入通道,在后一个进程建立输出通道,将数据从管道的左边传输到管道的右边,将 ls -l 的输出通过管道传给 “ grep *.c ”。

  进程创建管道,每次创建两个文件描述符来操作管道。其中一个对管道进行写操作,另一个描述符对管道进行读操作。如下图(用管道进行进程间通信)所示,显示了管道如何将两个进程通过内核连接起来,从下图(用管道进行进程间通信)中可以看出这两个文件描述符是如何连接在一起的。如果进程通过管道 fda[0] 发送数据,它可以从 fdb[0] 获得信息。
用管道进行进程间通信

  由于进程 A 和进程 B 都能够访问管道的两个描述符,因此管道创建完毕后要设置在各个进程中的方向,希望数据向那个方向传输。这需要做好规划,两个进程都要做统一的设置,在进程 A 中设置为读的管道描述符,在进程 B 中要设置为写;反之亦然,并且要把不关心的管道端关掉。对管道的读写与一般的 IO 系统函数一致,使用 write() 函数写入数据,read() 函数读出数据,某些特定的 IO 操作管道是不支持的,例如偏移函数 lseek()

2. pipe() 函数介绍

  创建管道的函数原型为:

#include<unistd.h> 
int pipe(int filedes[2]);

  数组中的 filedes 是一个文件描述符的数组,用于保存管道返回的两个文件描述符。数组中的第 1 个元素(下标为 0)是为了读操作而创建和打开的,而第 2 个元素(下标为 1)是为了写操作而创建和打开的。直观地说,fd1 的输出是 fd0 的输入。当函数执行成功时,返回 0;失败时返回值为 -1。建立管道的代码如下:

#include <stdio.h> 
#include <unistd.h> 
#include <sys/types.h> 

int main(void)
{
	int result = -1;	/* 创建管道结果 */
	int fd[2];
	
	result = pipe(fd);	/* 创建管道 */
	if( -1 == result)	/* 创建失败 */
	{
		printf("建立管道失败\n");	/* 打印信息 */
		return -1;				/* 返回错误 */
 	}
	... /* 正常程序处理过程 */
	...
}

  只建立管道看起来没有什么用处,要使管道有切实的用处,需要与进程的创建结合起来,利用两个管道在父进程和子进程之间进行通信。如下图(父子进程之间的通信)所示,在父进程和子进程之间建立一个管道,子进程向管道中写入数据,父进程从管道中读取数据。要实现这样的模型,在父进程中需要关闭写端,在子进程中需要关闭读端。

父子进程之间的通信

3. pipe() 函数的例子

  为了便于理解,建立两个变量 write_fdread_fd,分别指向 fd[1]fd[0],代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> 
#include <sys/types.h> 

int main(void)
{
	int result = -1;	/* 创建管道结果 */
	int fd[2]; 			/* 文件描述符,字符个数 */
	pid_t pid;			/* PID 值 */
	/* 文件描述符 1 用于写,文件描述符 0 用于读 */
	int *write_fd = &fd[1];	/* 写文件描述符 */
	int *read_fd = &fd[0];	/* 读文件描述符 */
	
	result = pipe(fd);	/* 建立管道 */
	if( -1 == result)	/* 建立管道失败 */
	{
		printf("建立管道失败\n"); /* 打印信息 */
		return -1;				/* 返回错误结果 */
	}
	
	pid = fork(); 	/* 分叉程序 */
	if( -1 == pid)	/* fork 失败 */
	{
		printf("fork 进程失败\n"); /* 打印信息 */
		return -1;	/* 返回错误结果 */
	}
	
	if( 0 == pid)	/* 子进程 */
	{
		close(*read_fd);	/* 关闭读端 */
	}
	else	/* 父进程 */
 	{
		close(*write_fd);	/* 关闭写端 */
	}
	return 0;
}

父子进程之间的通信

  如上图(父子进程之间的通信)所示的模型,在子进程中可以向管道写入数据,而写入的数据可以从父进程中读出。其完整的代码如下,子进程中向管道写入 “你好,管道”,父进程中读出这些信息。

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h>
#include <unistd.h>
#include <sys/types.h> 

int main(void)
{
	int result = -1; 	/* 创建管道结果 */ 
	int fd[2],nbytes; 	/* 文件描述符,字符个数 */
	pid_t pid; 		/* PID 值 */
	char string[] = "你好,管道";	
	char readbuffer[80];
	/* 文件描述符 1 用于写,文件描述符 0 用于读 */
	int *write_fd = &fd[1];	/* 写文件描述符 */ 
	int *read_fd = &fd[0];	/* 读文件描述符 */
	
	result = pipe(fd);	/* 建立管道 */ 
	if( -1 == result)	/* 建立管道失败 */
	{
		printf ("建立管道失败\n"); /* 打印倌息 */
		return -1; 	/* 返回错误结果 */
	}	 
	
	pid = fork(); 	/* 分叉程序 */
	if( -1 == pid) 	/* fork 失败 */
	{	 
		printf("fork 进程失败\n"); /* 打印信息 */
		return -1; 	/* 返回错误结果 */
	}	 
	if( 0 == pid)	/* 子进程 */
	{
		close(*read_fd);	/* 关闭读端 */
		result = write(*write_fd,string,strlen(string)); /* 向管道端写入宇符 */
		return 0;
	}
	else /* 父进程 */
	{
		close(*write_fd);	/* 关闭写端 */
		nbytes = read(*read_fd, readbuffer,sizeof(readbuffer)); /* 从管道读取数值 */
		printf("接收到 %d 个数据,内容为:\"%s\"\n",nbytes, readbuffer); /* 打印结果 */
	}
	return 0;
}

  运行的结果为:

接收到15个数据,内容为:“你好,管道”
4. 管道阻塞和管道操作的原子性

  当管道的写端口没有关闭时,如果写请求的字节数目大于阈值 PIPE_BUF,写操作的返回值是管道中目前的数据字节数。如果请求的字节数目不大于 PIPE_BUF,则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。

  ㊨ 注意:PIPE_BUFinclude/Linux/limits.h 中定义,不同的内核版本可能会有所不同。Posix.1 要求 PIPE_BUF 至少为 512 字节。

  管道进行写入操作的时候,当写入数据的数目小于 128K 时写入是非原子的,如果把父进程中的两次写入字节数都改为 128K,可以发现:写入管道的数据量大于 128K 字节时,缓冲区的数据将被连续地写入管道,直到数据全部写完为止,如果没有进程读数据,则一直阻塞。

5. 管道操作原子性的代码

  例如,下面的代码为一个管道读写的例子。在成功建立管道后,子进程向管道中写入数据,父进程从管道中读出数据。子进程一次写入 128K 字节的数据,父进程每次读取 10K 字节的数据。当父进程没有数据可读的时候退出。

#include <stdio.h>
#include <stdlib.h> 
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#define K 1024 
#define WRITELEN (128*K) 

int main(void)
{
	int result = -1;	/* 创建管道结果 */
	int fd[2],nbytes; 	/* 文件描述符,字符个数 */ 
	pid_t pid;		/* PID 值 */
	char string[WRITELEN]="你好,管道";
	char readbuffer[10*K];	/* 读缓冲区 */
	/* 文件描述符 1 用于写,文件描述符 0 用于读 */
	int *write_fd = &fd[1]; 
	int *read_fd = &fd[0];

	result = pipe(fd); 	/* 建立管道 */
	if( -1 == result)	/* 建立管道失败 */
	{
		printf("建立管道失败\n"); /* 打印信息 */
		return -1;		/* 返回错误结果 */
	}
	
	pid = fork(); /* 分叉程序 */
	if( -1 == pid)	/* fork 失败 */
	{ 
		printf("fork 进程失败\n");	/* 打印信息 */
		return -1;	/* 返回错误结果 */
	}	 
	if( 0 == pid) /* 子进程 */
	{
		int write_size = WRITELEN; /* 写入的长度 */
		result = 0; 	/* 结果 */
		close(*read_fd); /* 关闭读端 */
		while( write_size >= 0)	  /* 如果没有将数据写入继续操作 */
		{
			result = write(*write_fd, string, write_size); /* 写入管道数据 */
			if(result >0 )	/* 写入成功 */
			{
				write_size -= result;	/* 写入的长度 */
				printf("写入%d个数据,剩余%d个数据\n",result, write_size);
			}
			else	/* 写入失败 */
			{
				sleep(10);	/* 等待 10s,读端将数据读出 */
			}
		}
		return 0;
	}
	else	/* 父进程 */
	{
		close(*write_fd);	/* 关闭写端 */
		while(1)	/* 一直读取数据 */
		{
			nbytes = read(*read_fd, readbuffer,sizeof(readbuffer)); /* 读取数据 */
			if (nbytes <= 0)	/* 读取失败 */
			{
				printf("没有数据写入了\n");	/* 打印信息 */
				break;	/* 退出循环 */
			}
			printf("接收到 %d 个数据,内容为:“%s”\n",nbytes,readbuffer);
		}
	}
	return 0;
}
6. 管道原子性的例子运行结果

  将上述代码编译运行,其输出为:

接收到的 10240 个数据,内容为:“你好,管道”
接收到的 10240 个数据,内容为:“”
接收到的 10240 个数据,内容为:“”
接收到的 10240 个数据,内容为:“”
接收到的 10240 个数据,内容为:“”
接收到的 10240 个数据,内容为:“”
接收到的 10240 个数据,内容为:“”
接收到的 10240 个数据,内容为:“”
写入 131072 个数据,剩余 0 个数据
接收到的 10240 个数据,内容为:“”
接收到的 10240 个数据,内容为:“”
接收到的 10240 个数据,内容为:“”
接收到的 10240 个数据,内容为:“”
接收到的 8192 个数据,内容为:“”

  可以发现,父进程每次读取 10K 字节的数据,读了 13 次将全部数据读出。最后一次读数据,由于缓冲区中只有 8K 字节的数据,所以仅读取了 8K 字节。

  子进程一次性地写入 128K 字节的数据,当父进程将全部数据读取完毕的时候,子进程的 write() 函数才返回将写入信息(“写入 131072 个数据,剩余 0 个数据”)打印出来。

  上述操作证明管道的操作是阻塞性质的。

  使用管道需要注意以下 4 种特殊情况(假设都是阻塞 I/O 操作,没有设置 O_NONBLOCK 标志):

  1. 如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于 0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次 read 会返回 0,就像读到文件末尾一样。

  2. 如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于 0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次 read 会阻塞,直到管道中有数据可读了才读取数据并返回。

  3. 如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数等于 0),这时有进程向管道的写端 write,那么该进程会收到信号 SIGPIPE,通常会导致进程异常终止。讲信号时会讲到怎样使 SIGPIPE 信号不终止进程。

  4. 如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于 0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次 write 会阻塞,直到管道中有空位置了才写入数据并返回。

  管道的这四种特殊情况具有普遍意义。

  非阻塞管道, fcntl 函数设置 O_NONBLOCK 标志

fpathconf(int fd, int name) //测试管道缓冲区大小,_PC_PIPE_BUF

2. 命名管道(FIFO)

  命名管道的工作方式与普通的管道非常相似,但也有一些明显的区别。

  █ 在文件系统中命名管道是以设备特殊文件的形式存在的。
  █ 不同的进程可以通过命名管道共享数据。

1. 创建 FIFO(命名管道)

  有许多种方法可以创建命名管道。其中,可以直接用 shell 来完成。例如,在目录 /tmp 下建立一个名字为 namedfifo 的命名管道:

$ mkfifo /ipc/namedfifo 
$ ls -l /ipc/namedfifo
prw-rw-r-- 1 linux-c linux-c 0	5 月 31 22:56 /tmp/namedfifo

  可以看出 namedfifo 的属性中有一个 p,表示这是一个管道。
  为了用 C 语言创建 FIFO,用户可以使用 mkfifo() 函数。

#include <sys/types.h> 
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
2. FIFO 操作

  对命名管道 FIFO 来说,IO 操作与普通的管道 IO 操作基本上是一样的,二者之间存在着一个主要的区别。

  在 FIFO 中,必须使用一个 open() 函数来显式地建立连接到管道的通道。一般来说 FIFO 总是处于阻塞状态。也就是说,如果命名管道 FIFO 打开时设置了读权限,则读进程将一直“阻塞”,一直到其他进程打开该 FIFO 并且向管道中写入数据。这个阻塞动作反过来也是成立的,如果一个进程打开一个管道写入数据,当没有进程从管道中读取数据的时候,写管道的操作也是阻塞的,直到已经写入的数据被读出后,才能进行写入操作。如果不希望在进行命名管道操作的时候发生阻塞,可以在 open() 调用中使用 O_NONBLOCK 标志,以关闭默认的阻塞动作。

3. 消息队列

  消息队列是内核地址空间中的内部链表,通过 Linux 内核在各个进程之间传递内容。消息顺序地发送到消息队列中,并以几种不同的方式从队列中获取,每个消息队列可以用 IPC 标识符唯一地进行标识。内核中的消息队列是通过 IPC 的标识符来区别的,不同的消息队列之间是相对独立的。每个消息队列中的消息,又构成一个独立的链表

1. 消息缓冲区结构

  常用的结构是 msgbuf 结构。程序员可以以这个结构为模板定义自己的消息结构。在头文件 <linux/msg.h> 中,它的定义如下:

struct msgbuf 
{ 
	long mtype; 
	char mtext[1];
}

  在结构 msgbuf 中有以下两个成员。
  █ mtype:消息类型,以正数来表示。用户可以给某个消息设定一个类型,可以在消息队列中正确地发送和接收自己的消息。例如,在 socket 编程过程中,一个服务器可以接受多个客户端的连接,可以为每个客户端设定一个消息类型,服务器和客户端之间的通信可以通过此消息类型来发送和接收消息,并且多个客户端之间通过消息类型来区分。

  █ mtext:消息数据。
  消息数据的类型为 char,长度为 1。在构建自己的消息结构时,这个域并不一定要设为 char 或者长度为 1。可以根据实际的情况进行设定,这个域能存放任意形式的任意数据,应用程序编程人员可以重新定义 msgbuf 结构。例如:

struct msgmbuf
{
	long mtype; 
	char mtext[10]; 
	long length;
};

  上面定义的消息结构与系统模板定义的不一致,但是 mtype 是一致的。消息在通过内核在进程之间收发时,内核不对 mtext 域进行转换,任意的消息都可以发送。具体的转换工作是在应用程序之间进行的。但是,消息的大小,存在一个内部的限制。在 Linux 中, 它在 Linux/msg.h 中的定义如下:

#define MSGMAX 8192

  消息总的大小不能超过 8192 个字节,这其中包括 mtype 成员,它的长度是 4 个字节(long 类型)。

2. 结构 msgid_ds

  内核 msgid_ds 结构—— IPC 对象分为 3 类,每一类都有一个内部数据结构,该数据结构是由内核维护的。对于消息队列而言,它的内部数据结构是 msgid_ds 结构。对于系统上创建的每个消息队列,内核均为其创建、存储和维护该结构的一个实例。该结构在 Linux/msg.h 中定义,如下所示。

struct msgid_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 */
};

  为了叙述的完整性,下面对每个成员都给出一个简短的介绍。

  █ msg_perm:它是 ipc_perm 结构的一个实例,ipc_perm 结构是在 Linux/ipc.h 中定义 的。用于存放消息队列的许可权限信息,其中包括访问许可信息,以及队列创建者的有关信息(如 uid 等)。
  █ msg_stime:发送到队列的最后一个消息的时间戳(time_t)。
  █ msg_rime:从队列中获取最后一个消息的时间戳。
  █ msg_ctime:对队列进行最后一次变动的时间戳。
  █ msg_cbytes:在队列上所驻留的字节总数(即所有消息的大小的总和)。
  █ msg_qnum:当前处于队列中的消息数目。
  █ msg_qbytes:队列中能容纳的字节的最大数目。
  █ msg_lspid:发送最后一个消息进程的 PID
  █ msg_lrpid:接收最后一个消息进程的 PID

3. 结构 ipc_perm

  内核把 IPC 对象的许可权限信息存放在 ipc_perm 类型的结构中。例如在前面描述的某个消息队列的内部结构中,msg_perm 成员就是 ipc_perm 类型的,它的定义是在文件 <linux/ipc.h>,如下所示。

struct ipc_perm 
{
	key_t key;		/* 函数 msgget() 使用的键值 */
	uid_t uid;		/* 用户的 UID */
	gid_t gid;		/* 用户的 GID */
	uid_t cuid;		/* 建立者的 UID */
	gid_t cgid;		/* 建立者的 GID */
	unsigned short mode;	/* 权限 */
	unsigned short seq;		/* 序列号 */
}

  这个结构描述的主要是一些底层的东西,简单介绍如下。
  █ keykey 参数用于区分消息队列。
  █ uid:消息队列用户的 ID 号,
  █ gid:消息队列用户组的 ID 号。
  █ cuid:消息队列创建者的 ID 号。
  █ cgid:消息队列创建者的组 ID 号。
  █ mode:权限,用户控制读写,例如 0666,可以对消息进行读写操作。
  █ seq:序列号。

4. 内核中的消息队列关系

  作为 IPC 的消息队列,其消息的传递是通过 Linux 内核来进行的。如下图(消息机制在内核中的实现)所示的结构成员与用户空间的表述基本一致。在消息的发送和接收的时候,内核通过一个比较巧妙的设置来实现消息插入队列的动作和从消息中査找消息的算法。

消息机制在内核中的实现

  结构 list_head 形成一个链表,而结构 msg_msg 之中的 m_list 成员是一个 struct list_head 类型的变景,通过此变量,消息形成了一个链表,在查找和插入时,对 m_ist 域进行偏移操作就可以找到对应的消息体位置。内核中的代码在头文件 <linux/msg.h><linux/msg.c> 中,主要的实现是插入消息和取出消息的操作。

5. 键值构建 ftok() 函数

  ftok() 函数将路径名和项目的表示符转变为一个系统的 IPC 键值。其原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

  其中 pathname 必须是已经存在的目录,而 proj_id 则是一个 8 位(bit)的值,通常用 ab 等表示。例如建立如下目录:

$ mkdir -p /ipc/msg/

  然后用如下代码生成一个键值:

......
key_t key;
char *msgpath = "/ipc/msg/";	/* 生成魔数的文件路径 */
key = ftok(msgpath,'a');		/* 生成魔数 */
if(key != -1)					/* 成功 */
{
	printf("成功建立 KEY\n");
}
else	/* 失败 */
{
	printf("建立 KEY 失败\n");
}
......
6. 获得消息 msgget() 函数

  创建一个新的消息队列,或者访问一个现有的队列,可以使用函数 msgget(),其原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);

  msgget() 函数的第一个参数是键值,可以用 ftok() 函数生成,这个关键字的值将被拿来与内核中其他消息队列的现有关键字值相比较。比较之后,打开或者访问操作依赖于 msgflg 参数的内容。

  █ IPC_CREAT:如果在内核中不存在该队列,则创建它。
  █ IPC_EXCL:当与 IPC_CREAT 一起使用时,如果队列早已存在则将出错。

  如果只使用了 lPC_CREATmsgget() 函数或者返回新创建消息队列的消息队列标识符,或者会返回现有的具有同一个关键字值的队列的标识符。如果同时使用了 IPC_EXCLIPC_CREAT,那么将可能会有两个结果:创建一个新的队列,如果该队列存在,则调用将出错,并返回 -1IPC_EXCL 本身是没有什么用处的,但在与 IPC_CREAT 组合使用时,它可以用于保证没有一个现存的队列为了访问而被打开。例如,下面的代码创建一个消息队列:

......
key_t key;
int msg_flags, msg_id; 
msg_flags = IPC_CREAT|IPC_EXCL; 	/* 消息的标志为建立、可执行 */
mag_id = msgget(key, msg_flags|0x0666);	/* 建立消息 */
if( -1 == msg_id)	/* 建立消总失败 */
{
	printf("消息建立失败\n"); /* 打印信息 */
	return 0;	/* 退出 */
}
......
7. 发送消息 msgsnd() 函数

  一旦获得了队列标识符,用户就可以开始在该消息队列上执行相关操作了。为了向队列传递消息,用户可以使用 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);

  msgsnd() 函数的第 1 个参数是队列标识符,它是前面调用 msgget() 获得的返回值。第二个参数是 msgp,它是一个 void 类型的指针,指向一个消息缓冲区。msgsz 参数则包含着消息的大小,它是以字节为单位的,其中不包括消息类型的长度(4 个字节长)。

  msgflg 参数可以设置为 0(表示忽略),也可以设置为 IPC_NOWAIT。如果消息队列己满,则消息将不会被写入到队列中。如果没有指定 IPC_NOWAIT,则调用进程将被中断(阻塞),直到可以写消息为止。例如,如下代码向已经打开的消息队列发送消息:

......
struct msgmbuf		/* 消息的结构 */
{ 
	int mtype; 		/* 消息中的字节数 */
	char mtext[10];	/* 消息数据 */
};
int msg_sflags; /* 消息的标记 */
int msg_id;		/* 消息 ID 识别号 */
struct msgmbuf msg_mbuf; /* 建立消息结构变量 */
msg_sflags = IPC_NOWAIT; /* 直接读取消息,不等待 */ 
msg_mbuf.mtype = 10;	/* 消息的大小为 10 字节 */
memcpy(msg_mbuf.mtext,"测试消息",sizeof("测试消息")); /* 将数据复制如消息数据缓冲区 */
ret = msgsnd(msg_id, &msg_mbuf, sizeof("测试消息"),msg_sflags); /* 向消息 ID 发送消息 */
if(-1 == ret)	/* 发送消息失败 */
{
	printf("发送消息失败\n");	/* 打印消息 */
}
......

  首先将要发送的消息打包到 msg_mbuf.mtext 域中,然后调用 msgsnd 发送消息给内核。这里的 mtype 设置了类型为 10,当接受时必须设置此域为 10,才能接收到这时发送的消息。msgsnd() 函数的 msg_id 是之前 msgget 创建的。

8. 接收消息 msgrcv() 函数

  当获得队列标识符后,用户就可以开始在该消息队列上执行消息队列的接收操作。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);

  █ msgrcv() 函数的第 1 个参数 msqid 是用来指定,在消息获取过程中所使用的队列(该值是由前面调用 msgget() 得到的返回值)。
  █ 第 2 个参数 msgp 代表消息缓冲区变量的地址,获取的消息将存放在这里。
  █ 第 3 个参数 msgsz 代表消息缓冲区结构的大小,不包括 mtype 成员的长度。
  █ 第 4 个参数 msgtyp 指定要从队列中获取的消息类型。内核将查找队列中具有匹配类型的第一个到达的消息,并把它复制返回到由 msgp 参数所指定的地址中。如果 msgtyp 参数传送一个为 0 的值,则将返回队列中最老的消息,不管该消息的类型是什么。msgtyp=0:收到的第一条消息,任意类型。msgtyp>0:收到的第一条 msgtyp 类型的消息。msgtyp<0:收到的第一条最低类型(小于或等于 msgtyp 的绝对值)的消息。

  如果把 IPC_NOWAIT 作为一个标志传送给该函数,而队列中没有任何消息,则该次调用将会向调用进程返回 ENOMSG。否则,调用进程将阻塞,直到满足 msgrcv() 参数的消息到达队列为止。如果在客户等待消息的时候队列被删除了,则返回 EIDRM。如果在进程阻塞并等待消息的到来时捕获到一个信号,则返回 EINTR。函数 msgrcv 的使用代码如下:

msg_rflags = IPC_NOWAIT|MSG_NOERROR;	/* 消息接收标记 */
ret = msgrcv(msg _id, &msg_mbuf, 10,10,msg_rflags); /* 接收消息 */
if( -1 == ret)	/* 接收消息失败 */
{
	printf("接收消息失败\n");	/* 打印信息 */
}
else /* 接收消息成功 */
{
	printf("接收消息成功,长度:%d\n",ret); /* 打印信息 */
} 

  上面的代码中将 mtype 设置为 10,可以获得之前发送的内核的消息获得(因为之前发送的 mtype 值也设背为 10),msgrcv 返回值为接收到的消息长度。

9. 消息控制 msgctl() 函数

  通过前面的介绍已经知道如何在应用程序中简单地创建和利用消息队列。下面介绍一下如何直接地对那些与特定的消息队列相联系的内部结构进行操作。为了在一个消息队列上执行控制操作,用户可以使用 msgctl() 函数。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

  msgctl() 向内核发送一个 cmd 命令,内核根据此来判断进行何种操作,buf 为应用层和内核空间进行数据交换的指针。其中的 cmd 可以为如下值。

  █ IPC_STAT:获取队列的 msqid_ds 结构,并把它存放在 buf 变量所指定的地址中,通过这种方式,应用层可以获得当前消息队列的设置情况,例如是否有消息到来、消息队列的缓冲区设置等。
  █ IPC_SET:设置队列的 msqid_ds 结构的 ipc_perm 成员值,它是从 buf 中取得该值的。通过 IPC_SET 命令,应用层可以设置消息队列的状态,例如修改消息队列的权限,使其他用户可以访问或者不能访问当前的队列;甚至可以设置消息队列的某些当前值来伪装。
  █ IPC_RMID:内核删除队列。使用此命令执行后,内核会把此消息队列从系统中删除。

4. 消息队列的一个例子

  本例在建立消息队列后,打印其属性,并在每次发送和接收后均查看其属性,最后对消息队列进行修改。

1. 显示消息属性的函数 msg_show_attr()

  msg_show_attr() 函数根据用户输入的消息 ID,将消息队列中的字节数、消息数、最大字节数、最后发送消息的进程、最后接收消息的进程、最后发送消息的时间、最后接收消息的时间、最后消息变化的时间,以及消息的 UIDGID 等信息进行打印。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <unistd.h>
#include <time.h>
#include <sys/ipc.h>
/* 打印消息属性的函数 */
void msg_show_attr(int msg_id, struct msqid_ds msg_info)
{
	int ret = -1; 
	sleep(1);
	ret = msgctl(msg_id, IPC_STAT, &msg_info); /* 获取消息 */
	if( -1 == ret)
	{
		printf("获取消息信息失败!\n"); /* 获取消息失败,返回 */
		return ;
	}

	printf("\n"); /* 以下打印消息的信息 */
	printf("现在队列中的字节数:%ld\n",msg_info.msg_cbytes); /* 消息队列中的字节数 */
	printf("队列中消息数:%d\n",(int)msg_info.msg_qnum); /* 消息队列中的消息数 */
	printf("队列中最大字节数:%d\n", (int)msg_info.msg_qbytes); /* 消息队列中的最大字节数 */
	printf("最后发送消息的进程 pid: %d\n",msg_info.msg_lspid); /* 最后发送消息的进程 */
	printf("最后接收消息的进程 pid: %d\n",msg_info.msg_lrpid); /* 最后接收消息的进程 */
	printf("最后发送消息的时间:%s",ctime(&(msg_info.msg_stime))); /* 最后发送消息的时间 */
	printf("最后接收消息的时间:%s", ctime(&(msg_info.msg_rtime))); /* 最后接受消息的时间 */
	printf("最后变化时间:%s", ctime(&(msg_info.msg_ctime))); /* 消息的最后变化时间 */
	printf("消息 UID 是:%d\n",msg_info.msg_perm.uid); /* 消息的 UID */
	printf("消息 GID 是:%d\n",msg_info.msg_perm.gid); /* 消息的 GID */
}
2. 主函数 main()

  主函数先用函数 ftok() 使用路径 “/tmp/msg/b” 获得一个键值,之后进行相关的操作并打印消息的属性。

  █ 调用函数 msgget() 获得一个消息后,打印消息的属性;
  █ 调用函数 msgsnd() 发送一个消息后,打印消息的属性;
  █ 调用函数 msgrcv() 接收一个消息后,打印消息的属性;
  █ 最后,调用函数 msgctl() 并发送命令 IPC_RMID 销毁消息队列。

int main(void)
{
	int ret = -1; 
	int msg_flags, msg_id; 
	key_t key; 
	struct msgmbuf		/* 消息的缓冲区结构 */
	{
		int mtype; 
		char mtext[10];
	};
	struct msqid_ds msg_info; 
	struct msgmbuf msg_mbuf;

	int msg_sflags,msg_rflags; 
	char *msgpath = "/ipc/msg/"; 	/* 消息 key 产生所用的路径 */
	key = ftok(msgpath,'b');		/* 产生 key */
	if(key != -1)	/* 产生 key 成功 */
	{
		printf("成功建立 KEY\n");
	}
	else	/* 产生 key 失败 */
	{
		printf("建立 KEY 失败\n");
	}
	msg_flags = IPC_CREAT|IPC_EXCL;  /* 消息的类型 */
	msg_id = msgget(key, msg_flags|0x0666); /* 建立消息 */
	if( -1 == msg_id)
	{
		printf("消息建立失败\n");
		return 0;
	}
	msg_show_attr(msg_id, msg_info); /* 显示消息的属性 */
	
	msg_sflags = IPC_NOWAIT; 
	msg_mbuf.mtype = 10;
	memcpy(msg_mbuf.mtext,"测试消息",sizeof("测试消息")); /* 复制字符串 */
	ret = msgsnd(msg_id, &msg_mbuf, sizeof("测试消息"),msg_sflags); /* 发送消息 */
	if( -1 == ret)
	{
		printf("发送消息失败\n");
	}
	msg_show_attr(msg_id,msg_info);	/* 显示消息属性 */
	
	msg_rflags = IPC_NOWAIT|MSG_NOERROR;
	ret = msgrcv(msg_id, &msg_mbuf, 10,10,msg_rflags);	/* 接收消息 */
	if( -1 == ret)
	{
		printf("接收消息失败\n");
	}
	else
	{
		printf("接收消息成功,长度:%d\n",ret);
	}
	msg_show_attr(msg_id, msg_info); /* 显示消息属性 */

	msg_info.msg_perm.uid =8;
	msg_info.msg_perm.gid = 8;
	msg_info.msg_qbytes = 12345;
	ret = msgctl(msg_id, IPC_SET, &msg_info); /* 设置消息属性 */
	if( -1 == ret)
	{
		printf("设置消息属性失败\n");
		return 0;
	}
	msg_show__attr(msg_id, msg_info); /* 显示消息属性 */
	
	ret = msgctl(msg_id, IPC_RMID,NULL); /* 删除消息队列 */
	if(-1 == ret) 
	{
		printf("删除消息失败\n");
		return 0;
	}
	return 0;
}

5. 信号量

  信号量是一种计数器,用来控制对多个进程共享的资源所进行的访问。它们常常被用做一个锁机制,在某个进程正在对特定资源进行操作时,信号量可以防止另一个进程去访 问它。生产者和消费者的模型是信号量的典型使用。

  接下来将介绍信号量的概念和常用的函数,并对信号量进行包装形成一整套用户可以理解的信号量函数。

1. 信号量数据结构

  信号量数据结构是信号量程序设计中经常使用的数据结构,由于在之后的函数中经常用到,这里将结构的原型列出来,便于读者査找。

union semun /* 信号量操作的联合结构 */
{
	int	val;	/* 整型变量 */
	struct semid_ds *buf; 	/* semid_ds 结构指针 */
	unsigned short *array; 	/* 数组类型 */
	struct seminfo *_buf;	/* 信号量内部结构 */
};
2. 新建信号量函数 semget()

  semget() 函数用于创建一个新的信号量集合,或者访问现有的集合。其原型如下,其中第 1 个参数 keyftok 生成的键值,第 2 个参数 nsems 参数可以指定在新的集合中应该创建的信号量的数目,第 3 个参数 semflsg 是打开信号量的方式。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

  semflsg 是打开信号量的方式。
  █ IPC_CREAT:如果内核中不存在这样的信号量集合,则把它创建出来。
  █ IPC_EXCL:当与 IPC_CREAT 一起使用时,如果信号量集合早已存在,则操作将失败。如果单独使用 IPC_CREATsemget() 或者返回新创建的信号量集合的信号量集合标识符;或者返回早己存在的具有同一个关键字值的集合的标识符。如果同时使用 IPC_EXCLIPC_CREAT,那么将有两种可能的结果:如果集合不存在,则创建一个新的集合;如果集合早己存在,则调用失败,并返回 -1IPC_EXCL 本身是没有什么用处的,但当与 IPC_CREAT组合使用时,它可以用于防止为了访问而打开现有的信号量集合。

  利用 semget() 函数包装建立信号量的代码如下:

typedef int sem_t;
union semun /* 信号量操作的联合结构 */
{		
	int	val;	/* 整型变量 */
	struct semid_ds	*buf;	/* semid_ds 结构指针 */
	unsigned short	*array;	/* 数组类 */
} arg;	/* 定义一个全局变量 */
sem_t createSem(key_t key, int value) /* 建立信号量,魔数 key 和信号量的初始值 value */
union semun sem; /* 信号量结构变量 */ 
sem_t semid; /* 信号量 ID */
sem.val = value; /* 设置初始值 */

semid = semget(key,0,IPC_CREAT|0666); /* 获得信号量的 ID */
if (-1 == semid) /* 获得信号量 ID 失败 */
{
	printf("create semaphore error\n"); /* 打印信息 */
	return -1; /* 返回错误 */
}
	semetl(semid,0,SETVAL,sen);	/* 发送命令,建立 value 个初始值的信号量 */

	return semid; /* 返回建立的信号量 */
}

  CreateSem() 函数按照用户的键值生成个信号量,把信号量的初始值设为用户输入的 value

3. 信号量操作函数 semop()

  信号量的 PV 操作是通过向已经建立好的信号量(使用 semget() 函数),发送命令来完成的。向信号量发送命令的函数是 semop(),这个函数的原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);

  semop() 函数第 2 个参数(sops)是一个指针,指向将要在信号量集合上执行操作的一个数组,而第 3 个参数(nsops)则是该数组中操作的个数。sops 参数指向的是类型为 sembuf 结构的一个数组。sembuf 结构是在 linux/sem.h 中定义的,如下所示。

struct sembuf
{
	ushort sem_num; /* 信号量的编号 */
	short sem_op;  /* 信号量的操作 */
	short sem_flg; /* 信号量的操作标志 */
};

  █ sem_num:用户要处理的信号量的编号。
  █ sem_op:将要执行的操作(正、负,或者零)。
  █ sem_flg:信号量操作的标志。如果 sem_op 为负,则从信号量中减掉一个值。如果 sem_op 为正,则从信号量中加上值。如果 sem_op0,则将进程设置为睡眠状态,直到信号量的值为 0 为止。

  例如 “ struct sembuf sem={0,+1,NOWAIT}; ” 表示对信号量 0,进行加 1 的操作。用函数 semop() 可以构建基本的 PV 操作,代码如下所示。Sem_P 构建 {0, +1, NOWAIT}sembuf 结构来进行增加 1 个信号量值的操作;Sem_V 构建 {0, -1,NOWAIT}sembuf 结构来进行减少 1 个信号量的操作,所对应的信号量由函数传入(semid)。

int Sem_P(sem_t semid) /* 增加信号量 */
{
	struct sembuf sops={0,+1,IPC_NOWAIT}; /* 建立信号量结构值 */
	
	return (semop(semid,&sops,1)); /* 发送命令 */
}
int Sem_V(sem_t semid) /* 减小信号量值 */
{
	struct sembuf sops={0,-1,IPC_NOWAIT}; /* 建立信号量结构值 */
	
	return (semop(semid,&sops,1)); /* 发送信号量操作方法 */
 }
4. 控制信号量参数 semctI() 函数

  与文件操作的 ioctl() 函数类似,信号量的其他操作是通过函数 semctl() 来完成的。函数 semctl() 的原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);

  函数 semctI() 用于在信号量集合上执行控制操作。这个调用类似于函数 msgctl()msgctl() 函数是用于消息队列上的操作。semctl() 函数的第 1 个参数是关键字的值(在我的例子中它是调用 semget() 函数所返回的值)。第 2 个参数(semun)是将要执行操作的信号量的编号,它是信号量集合的一个索引值,对于集合中的第 1 个信号量(有可能只有这一个信号量)来说,它的索引值将是一个为 0 的值。cmd 参数代表将要在集合上执行的命令。其取 值如下所述。

  █ IPC_STAT:获取某个集合的 semid_ds 结构,并把它存储在 semun 联合体的 buf 参数所指定的地址中。
  █ IPC_SET:设置某个集合的 semid_ds 结构的 ipc_perm 成员的值。该命令所取的值是从 semun 联合体的 buf 参数中取到的。
  █ IPC_RMID:从内核删除该集合。
  █ GETALL:用于获取集合中所有信号量的值。整数值存放在无符号短整数的一个数组中,该数组由联合体的 array 成员所指定。
  █ GETNCNT:返回当前正在等待资源的进程的数目。
  █ GETPID:返回最后一次执行 semop 调用的进程的 PID
  █ GETVAL:返回集合中某个信号量的值。
  █ GETZCNT:返回正在等待资源利用率达到百分之百的进程的数目。
  █ SETALL:把集合中所有信号量的值,设置为联合体的 array 成员所包含的对应值。
  █ SETVAL:把集合中单个信号量的值设置为联合体的 val 成员的值。

  参数 arg 代表类型 semun 的一个实例。这个特殊的联合体是在 Linux/sem.h 中定义的,如下所示。

  █ vah:当执行 SETVAL 命令时将用到这个成员,它用于指定要把信号量设置成什么值。
  █ buf:在命令 IPC_STAT/IPC_SET 中使用。它代表内核中所使用的内部信号量数据 结构的一个复制。
  █ array:用在 GETALL/SETALL 命令中的一个指针。它应当指向整数值的一个数组。在设置或获取集合中所有信号最的值的过程中,将会用到该数组。
  █ 剩下的参数,_buf_pad 将在内核中的信号暈代码的内部使用,对于应用程序开发人员来说,它们用处很少,或者说没有用处。这两个参数是 Linux 操作系统所特有的,在其他的 UNIX 实现中没有。

  利用 semctl() 函数设置和获得信号量的值构建通用的函数:

void SetvalueSem(sem_t semid, int value) /* 设置信号量的值 */
{
	union semun sem; /* 信号量操作的结构 */
	sem.val = value; /* 值初始化 */
	
	semctl(semid,0,SETVAL,sem); /* 设置信号量的值 */
}
int GetvalueSem(sem_t semid) /* 获得信号量的值 */
{
	union semun sem; /* 信号量操作的结构 */ 
	return semctl(semid,0,GETVAL, sem); /* 获得信号量的值 */
}

  SetvalueSem() 函数设置信号量的值,它是通过 SETVAL 命令实现的,所设置的值通过联合变量 semval 域实现。GetvalueSem() 函数用于获得信号量的值,semctl() 函数的命令 GETVAL 会使其返回给定信号量的当前值。当然,销毁信号量同样可以使用 semctl() 函数实现。

void DestroySem(sem_t semid) /* 销毁信号量 */
{
	union semun sem;	/* 信号量操作的结构 */
	sem.val = 0;	/* 信号量值的初始化 */
	semctl(semid,0,IPC_RMID,sem);	/* 设置信号量 */
}

  命令 IPC_RMID 将给定的信号量销毁。

5. 一个信号量操作的例子

  在之前的信号量函数的基础上,进行了单进程的信号量程序模拟。下面的代码先建立一个信号量,然后对这个信号量进行 PV 操作,并将信号量的值打印出来,最后销毁信号量。

int main(void)
{
	key_t key; /* 信号量的键值 */
	int semid; /* 信号量的 ID */
	char i;
	int value = 0;
	
	key = ftok("/ipc/sem",'a'); /* 建立信号量的键值 */
	semid = CreateSem(key,100); /* 建立信号量 */
	for (i = 0;i <= 3;i++) /* 对信号量进行 3 次增减操作 */
	{ 
		Sem_P(semid); /* 增加信号量 */
		Sem_V(semid); /* 减小信号量 */
	}
	value = GetvalueSem(semid);  /* 获得信号量的值 */
	printf("信号量值为:%d\n",value); /* 打印结果 */
	DestroySem(semid); /* 销毁信号量 */
	return 0;
}

6. 共享内存

  共享内存是在多个进程之间共享内存区域的一种进程间的通信方式,它是在多个进程之间对内存段进行映射的方式实现内存共享的。这是 IPC 最快捷的方式,因为共享内存方式的通信没有中间过程,而管道、消息队列等方式则是需要将数据通过中间机制进行转换;与此相反,共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅是地址不同而己,因此不需要进行复制,可以直接使用此段空间。

1. 创建共享内存函数 shmget()

  函数 shmget() 用于创建一个新的共享内存段,或者访问一个现有的共享内存段,它与消息队列,以及信号量集合对应的函数十分相似。函数 shmget() 的原型如下:

#include <sys/ipc_h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

  shmget() 的第一个参数是关键字的值。然后,这个值将与内核中现有的其他共享内存段的关键字值相比较。在比较之后,打开和访问操作都将依赖于 shmflg 参数的内容。

  █ IPC_CREAT:如果在内核中不存在该内存段,则创建它。
  █ IPC_EXCL:当与 IPC_CREAT 一起使用时,如果该内存段早已存在,则此次调用将失败。

  如果只使用 IPC_CREATshmget() 或者将返回新创建的内存段的段标识符,或者返回早已存在于内核中的具有相同关键字值的内存段的标识符。如果同时使用 IPC_CREATIPC_EXCL,则可能会有两种结果:如果该内存段不存在,则将创建一个新的内存段;如果内存段早已存在,则此次调用失败,并将返回 -1IPC_EXCL 本身是没有什么用处的,但在与 IPC_CREAT 组合使用时,它可用于防止一个现有的内存段为了访问而打开着。一旦进程获得了给定内存段的合法 IPC 标识符,它的下一步操作就是连接该内存段,或者把该内存段映射到自己的寻址空间中。

2. 获得共享内存地址函数 shmat()

  函数 shmat() 用来获取共享内存的地址,获取共享内存成功后,可以像使用通用内存一样对其进行读写操作。函数的原型如下:

#include <sys/types.h> 
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg); 
int shmdt(const void *shmaddr);

  如果 shmaddr 参数值等于 0,则内核将试着査找一个未映射的区域。用户可以指定一个地址,但通常该地址只用于访问所拥有的硬件,或者解决与其他应用程序的冲突。SHM_RND 标志可以与标志参数进行 OR 操作,结果再置为标志参数,这样可以让传送的地址页对齐(舍入到最相近的页面大小)。

  此外,如果把 SHM_RDONLY 标志与标志参数进行 OR 操作,结果再置为标志参数,这样映射的共享内存段只能标记为只读方式。

  当申请成功时,对共享内存的操作与一般内存一样,可以直接进行写入和读出,以及偏移的操作。

3. 删除共享内存函数 shmdt()

  函数 shmdt() 用删除一段共享内存。函数的原型如下:

#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);

  当某进程不再需要一个共享内存段时,它必须调用这个函数来断开与该内存段的连接。正如前面所介绍的那样,这与从内核删除内存段是两回事。在成功完成了断开连接操作以后,相关的 shmid_ds 结构的 shm_nattch 成员的值将减去 1。如果这个值减到 0,则内核将真正删除该内存段。

4. 共享内存控制函数 shmctl()

  共享内存的控制函数 shmctl() 的使用类似 ioctl() 的方式对共享内存进行操作:向共享内存的句柄发送命令来完成某种功能。函数 shmctl() 的原型如下,其中 shmid 是共享内存的句柄,cmd 是向共享内存发送的命令,最后一个参数 buf 则是向共享内存发送命令的参数。

#include <sys/ipc.h> 
#include <sys/shm.h> 
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

  结构 shmid_ds 结构定义如下:

struct shmid ds
{			
	struct ipc_perm	shm_perm;	/* 所有者和权限 */
	size_t	shm_segsz;	/* 段大小,以字节为单位 */
	time_t	shm_atime;	/* 最后挂接时间 */
	time_t	shm_dtime;	/* 最后取出时间 */
	time_t	shm_ctime;	/* 最后修改时间 */
	pid_t	shm_cpid;	/* 建立者的 PID */
	pid_t	shm_lpid;	/* 最后调用函数 shmat()/shmdt() 的 PID */	
	shmatt_t	shm_nattch;	/* 现在挂接的数量 */
	......
}

  此函数与消息队列的 msgctl() 函数调用是完全类似的,它的合法命令值是如下所述。

  █ IPC_SET:获取内存段的 shmid_ds 结构,并把它存储在 buf 参数所指定的地址中。IPC_SET 设置内存段 shmid_ds 结构的 ipc_perm 成员的值,此命令是从 buf 参数中获得该值的。
  █ IPC_RMID:标记某内存段,以备删除。该命令并不真正地把内存段从内存中删除。相反,它只是标记上该内存段,以备将来删除。只有当前连接到该内存段的最后一个进程正确地断开了与它的连接,实际的删除操作才会发生。当然,如果当前没有进程与该内存段相连接,则删除将立刻发生。为了正确地断开与其共享内存段的连接,进程需要调用 shmdt() 函数。

5. —个共享内存的例子

  下面的代码在父进程和子进程之间利用共享内存进行通信,父进程向共享内存中写入数据,子进程读出数据。两个进程之间的控制采用了信号量的方法,父进程写入数据成功后,信号量加 1,子进程在访问信号量之前先等待信号。

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

static char msg[]="你好,共享内存\n";

int main(void)
{
	key_t key; 
	int semid,shmid; 
	char i,*shms,*shmc; 
	struct semid_ds buf; 
	int value = 0; 
	char buffer[80]; 
	pid_t p;
	key = ftok("/ipc/sem",'a');	/* 生成键值 */
	shmid = shmget(key,1024,IPC_CREAT|0604); /* 获得共享内存,大小为 1024 个字节 */
	
	semid = CreateSem(key,0); /* 建立信号量 */
	
	p = fork (); /* 分叉程序 */
	if(p > 0) /* 父进程 */
	{
		shms = (char *)shmat(shmid,0,0); /* 挂接共享内存 */
		memcpy (shms, msg, strlen(msg)+1);	/* 复制内容 */
		sleep(10);	/* 等待 10s,另一个进程将数据读出 */ 
		Sem_P(semid); /* 获得共享内存的信号量 */ 
		shmdt(shms);	/* 摘除共享内存 */
		
		DestroySem(semid); /* 销毁信号量 */
	}
	else if(p == 0) /* 子进程 */
	{
		shmc = (char *)shmat(shmid,0,0); /* 挂接共享内存 */
		Sera_V(semid); /* 减小信号量 */
		printf("共享内存的值为:%s\n", shmc); /* 打印信息 */
		shmdt(shmc); /* 摘除共享内存 */
	}
	return 0;
}

7. 信号

  信号(signal)机制是 UNIX 系统中最为古老的进程之间的通信机制。它用于在一个或多个进程之间传递异步信号。信号可以由各种诗步事件产生,例如键盘中断等。Shell 也可以使用信号将作业控制命令传递给它的子进程。

  Linux 系统中定义了一系列的信号,这些信号可以由内核产生,也可以由系统中的其他进程产生,只要这些进程有足够的权限。可以使用 kill 命令(kill -l)在机器上列出所有的信号,如下所示:

$ kill -l
 1) SIGHUP    2) SIGINT    3) SIGQUIT    4) SIGILL		 5) SIGTRAP
 6) SIGABRT   7) SIGBUS	   8) SIGFPE	 9) SIGKILL 	10) SIGUSR1
11) SIGSEGV	  12) SIGUSR2 13) SIGPIPE 14) SIGALRM   15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN   22) SIGTTOU   23) SIGURG   24) SIGXCPU  25) SIGXFSZ
26) SIGVTALRM  27) SIGPROF  28) SIGWINCH  29) SIGIO  30) SIGPWR
31) SIGSYS     34) SIGRTMIN  35) SIGRTMIN+1  36) SIGRTMIN+2   37) SIGRTMIN+3
38) SIGRTMIN+4   39) SIGRTMIN+5    40) SIGRTMIN+6    41) SIGRTMIN+7    42) SIGRTMIN+8
43) SIGRTMIN+9   44) SIGRTMIN+10    45) SIGRTMIN+11   46) SIGRTMIN+12  47) SIGRTMIN+13
48) SIGRTMIN+14  49) SIGRTMIN+15    50) SIGRTMAX-14   51) SIGRTMAX-13  52) SIGRTMAX-12
53) SIGRTMAX-11  54) SIGRTMAX-10    55) SIGRTMAX-9    56) SIGRTMAX-8   57) SIGRTMAX-7
58) SIGRTMAX-6   59) SIGRTMAX-5     60) SIGRTMAX-4    61) SIGRTMAX-3   62) SIGRTMAX-2
63) SIGRTMAX-1   64) SIGRTMAX

  进程可以屏蔽掉大多数的信号,除了 SIGSTOPSIGKILLSIGSTOP 信号使一个正在运行的进程暂停,而信号 SIGKILL 则使正在运行的进程退出。进程可以选择系统的默认方式处理信号,也可以选择自己的方式处理产生的信号。信号之间不存在相对的优先权,系统也无法处理同时产生的多个同种的信号,也就是说,进程不能分辨它收到的是 1 个或 者是 42SIGCONT 信号。

  █ SIGABRT:调用 abort() 函数时产生此信号,进程异常终止。
  █ SIGALRM:超过用 alarm() 函数设置的时间时产生此信号。
  █ SIGBUS:指示一个实现定义的硬件故障。
  █ SIGCHLD:在一个进程终止或停止时,SIGCHLD 信号被送给其父进程。如果希望从父进程中了解其子进程的状态改变,则应捕捉此信号。信号捕捉函数中通常要调用 wait() 函数以取得子进程 ID 和其终止状态。
  █ SIGCONT:此作业控制信号送给需要继续运行的处于停止状态的进程。如果接收到此信号的进程处于停止状态,则操作系统的默认动作是使该停止的进程继续运行,否则默认动作是忽略此信号。
  █ SIGEMT:指示一个实现定义的硬件故障。
  █ SIGFPE:此信号表示一个算术运算异常,例如除以 0,浮点溢出等。
  █ SIGHUP:如果终端界面检测到一个连接断开,则将此信号送给与该终端相关的进程。
  █ SIGILL:此信号指示进程己执行一条非法硬件指令。
  █ SIGINT:当用户按中断键(一般采用 DeleteCtrl+C)时,终端驱动程序产生这个信号并将信号送给前台进程组中的每一个进程。当一个进程在运行时失控,特别是它正在屏幕上产生大量不需要的输出时,常用此信号终止它。
  █ SIGIO:此信号指示一个异步 IO 事件。
  █ SIGIOT:这指示一个实现定义的硬件故障。
  █ SIGPIPE:如果在读进程时已终止写管道,则产生此信号。
  █ SIGQUIT:当用户在终端上按退出键(一般采用 Ctrl+C)时,产生此信号,并送至前台进程组中的所有进程。
  █ SIGSEGV:指示进程进行了一次无效的存储访问。
  █ SIGSTOP:这是一个作业控制信号,它停止一个进程。
  █ SIGSYS:指示一个无效的系统调用。由于某种未知原因,某个进程执行了一条系统调用命令,但是调用命令所用的参数无效。
  █ SIGTERM:这是由kiH命令发送的系统默认终止信号。
  █ SIGTRAP:指示一个实现定义的硬件故障。
  █ SIGTSTP:交互停止信号,当用户在终端上按挂起键(一般采用 Ctrl+Z)时,终端驱动程序产生此信号。
  █ SIGTTIN:当一个后台进程组进程试图读其控制终端时,终端驱动程序产生此 信号。
  █ SIGTTOU:当一个后台进程组进程试图写其控制终端时产生此信号。
  █ SIGURG:此信号通知进程已经发生一个紧急情况。在网络连接上,接到非规定波特率的数据时,此信号可选择地产生。
  █ SIGUSR1:这是一个用户定义的信号,可用于应用程序。
  █ SIGUSR2:这是一个用户定义的信号,可用于应用程序。

1. 信号机制
$ man  7 signal
......
Term 	Default action is to terminate the process. # 默认操作是终止进程。
Ign 	Default action is to ignore the signal. # 默认的操作是忽略这个信号。
Core 	Default action is to terminate the process and dump core (see core(5)). # 默认操作是终止进程并转储core(参见core(5))。
Stop 	Default action is to stop the process. # 默认操作是停止进程
Cont 	Default action is to continue the process if it is currently stopped. # 如果当前进程已停止,则默认操作是继续该过程。
......
 First the signals described in the original POSIX.1-1990 standard.

       Signal     Value     Action   Comment
       ──────────────────────────────────────────────────────────────────────
       SIGHUP        1       Term    Hangup detected on controlling terminal
                                     or death of controlling process
       SIGINT        2       Term    Interrupt from keyboard

       SIGQUIT       3       Core    Quit from keyboard
       SIGILL        4       Core    Illegal Instruction
       SIGABRT       6       Core    Abort signal from abort(3)
       SIGFPE        8       Core    Floating-point exception
       SIGKILL       9       Term    Kill signal
       SIGSEGV      11       Core    Invalid memory reference
       SIGPIPE      13       Term    Broken pipe: write to pipe with no
                                     readers; see pipe(7)
       SIGALRM      14       Term    Timer signal from alarm(2)
       SIGTERM      15       Term    Termination signal
       SIGUSR1   30,10,16    Term    User-defined signal 1
       SIGUSR2   31,12,17    Term    User-defined signal 2
       SIGCHLD   20,17,18    Ign     Child stopped or terminated
       SIGCONT   19,18,25    Cont    Continue if stopped
       SIGSTOP   17,19,23    Stop    Stop process
       SIGTSTP   18,20,24    Stop    Stop typed at terminal
       SIGTTIN   21,21,26    Stop    Terminal input for background process
       SIGTTOU   22,22,27    Stop    Terminal output for background process

       The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.

......

  表中第一列是各信号的宏定义名称,第二列是各信号的编号,第三列是默认处理动作:

Term	表示终止当前进程.
Core	表示终止当前进程并且Core Dump(Core Dump 用于gdb调试).
Ign		表示忽略该信号.
Stop	表示停止当前进程.
Cont	表示继续执行先前停止的进程.

  表中最后一列是简要介绍,说明什么条件下产生该信号。

2. 信号产生种类

  终端特殊按键

ctl+c SIGINT	发送 SIGINT 信号给前台进程组中的所有进程。常用于终止正在运行的程序。
ctl+z SIGTSTP	发送 SIGTSTP 信号给前台进程组中的所有进程,常用于挂起一个进程。
ctl+\ SIGQUIT	发送 SIGQUIT 信号给前台进程组中的所有进程,终止前台进程并生成 core 文件。
ctrl+d 	不是发送信号,而是表示一个特殊的二进制值,表示 EOF。

  硬件异常
  █ 除 0 操作
  █ 访问非法内存

3. 信号产生原因
1) SIGHUP:当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程
2)SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止里程。
3)SIGQUIT:当用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。
4)SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件
5)SIGTRAP:该信号由断点指令或其他trap指令产生。默认动作为终止里程并产生core文件。
6 ) SIGABRT:调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
7)SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
8)SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。
9)SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
10)SIGUSE1:用户定义的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
11)SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。
12)SIGUSR2:这是另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。1
13)SIGPIPE:Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。
14) SIGALRM:定时器超时,超时的时间由系统调用alarm设置。默认动作为终止进程。
15)SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号。默认动作为终止进程。
16)SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。
17)SIGCONT:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为终止进程。
18)SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。
19)SIGTSTP:停止进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。
21)SIGTTOU:该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。
22)SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。
23)SIGXFSZ:进程执行时间超过了分配给该进程的CPU时间,系统产生该信号并发送给该进程。默认动作为终止进程。
24)SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。
25)SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程。
26)SGIPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程。
27)SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。
28)SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略。
29)SIGPWR:关机。默认动作为终止进程。
30)SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件。
31)SIGRTMIN~(64)SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。
4. 信号函数集合
1int sigemptyset(sigset_t *set);

  该函数的作用是将信号集初始化为空。

2int sigfillset(sigset_t *set);

  该函数的作用是把信号集初始化包含所有已定义的信号。

3int sigaddset(sigset_t *set, int signo);

  该函数的作用是把信号 signo 添加到信号集 set 中,成功时返回 0,失败时返回 -1

4int sigdelset(sigset_t *set, int signo);

  该函数的作用是把信号 signo 从信号集 set 中删除,成功时返回 0,失败时返回 -1.

5int sigismember(sigset_t *set, int signo);

  该函数的作用是判断给定的信号signo是否是信号集中的一个成员,如果是返回 1,如果不是,返回 0,如果给定的信号无效,返回 -1

6int sigpromask(int how, const sigset_t *set, sigset_t *oset);

  该函数可以根据参数指定的方法修改进程的信号屏蔽字。新的信号屏蔽字由参数 set(非空)指定,而原先的信号屏蔽字将保存在 oset(非空)中。如果 set 为空,则 how 没有意义,但此时调用该函数,如果 oset 不为空,则把当前信号屏蔽字保存到 oset 中。

  how 的不同取值及操作如下所示:

how 的取值对应操作
SIG_BLOCK把参数 set 中的信号添加到信号屏蔽字中
SIG_SETKASK把信号屏蔽字设置为参数 set 中的信号
SIG_UNBLOCK从信号屏蔽字中刪除参数 set 中的信号

  如果 sigpromask 成功完成返回 0,如果 how 取值无效返回 -1,并设置 errnoEINVAL

  注意:调用这个函数才能改变进程的屏蔽字,之前的函数都是为改变一个变量的值而已,并不会真正影响进程的屏蔽字。

7int sigpending(sigset_t *set);

  该函数的作用是将被阻塞的信号中停留在待处理状态的一组信号写到参数 set 指向的信号集中,成功调用返回 0,否则返回 -1,并设置 errno 表明错误原因。

8int sigsuspend(const sigset_t *sigmask);

  该函数通过将进程的屏蔽字替换为由参数 sigmask 给出的信号集,然后挂起进程的执行。注意操作的先后顺序,是先替换再挂起程序的执行。程序将在信号处理函数执行完毕后继续执行。如果接收到信号终止了程序,sigsuspend() 就不会返回,如果接收到的信号没有终止程序,sigsuspend() 就返回 -1,并将 errno 设置为 EINTR

  特别提醒:如果一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态,当进程解除对待处理信号的阻塞时,待处理信号就会立刻被处理。

5. 信号截取函数 signal()

  signal() 函数用于截取系统的信号,对此信号挂接用户自己的处理函数。其原型如下:

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

  signal() 函数的原型说明此函数要求两个参数,返回一个函数指针,而该指针所指向的函数无返回值(void)。第 1 个参数 signo 是一个整型数,第 2 个参数是函数指针,它所指向的函数需要一个整型参数,无返回值。用一般语言来描述就是要向信号处理程序传送一个整型参数,而它却无返回值。当调用 signal 设置信号处理程序时,第 2 个参数是指向该函数(也就是信号处理程序)的指针。signal 的返回值指向以前信号处理程序的指针。

  如下代码截取了系统的信号 SIGSTOPSIGKILL,用命令 kill 杀死其是不可能的。

#include <signal.h>
#include <stdio.h>

typedef void (*sighandler_t)(int);
static void sig_handle(int signo) /* 信号处理函数 */
{
	if( SIGSTOP == signo) /* 为 SIGSTOP 信号 */
	{
		printf("接收到信号 SIGSTOP\n"); /* 打印信息 */
	}
	else if(SIGKILL == signo) /* 为 SIGKILL 信号 */
	{
		printf("接收到信号 SIGKILL\n"); /* 打印信息 */
	}
	else /* 其他信号 */
	{
		printf("接收到信号:%d\n", signo); /* 打印信息 */
	}
 	return;
}

int main(void)
{
	sighandler_t ret;
	ret = signal(SIGSTOP, sig_handle);	/* 挂接 SIGSTOP 信号处理函数 */
	if(SIG_ERR == ret)	/* 挂接失败 */
	{
		printf("为 SIGSTOP 挂接信号处理函数失败\n");
		return -1;	/* 返回 */
	}
	
	ret = signal(SIGK1LL, sig_handle);	/* 挂接 SIGKILL 处理函数 */
	if(SIG ERR == ret)	/* 挂接失败 */
	{
		printf("为 SIGKILL 挂接信号处理函数失败\n");
		return -1;	/* 返回 */
	}
	for (;;);	/* 等待程序退出 */
}
6. 向进程发送信号函数 kill() 和 raise()

  在挂接信号处理函数后。可以等待系统信号的到来。同时,用户可以自己构建信号发送到目标进程中。此类函数有 kill()raise() 函数,函数的原型如下:

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
int raise(int sig);

  kill() 函数向进程号为 pid 的进程发送信号,信号值为 sig。当 pid0 时,向当前系统的所有进程发送信号 sig,即 “群发” 的意思。raise() 函数在当前进程中自举一个信号 sig,即向当前进程发送信号。

  ㊨ 注意:kill() 函数的名称虽然是 “杀死” 的意思,但是它并不是杀死某个进程,而是向某个进程发送信号,这个信号除了 SIGSTOPSIGKILL,—般不会使进程显式地退出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值