进程间通信和同步


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

半双工管道

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

基本概念

        由于管道仅仅是将某个进程的输出和另一个进程的输入相连接的单向通信的办法,因此称其为”半双工”。在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()。

off_t currpos;
currpos = lseek(fd,0,SEEK_CUR)
对lseek的成功调用会返回心得文件偏移量,如果文件描述符file descriptor指向的是一个pipe,FIFO,socket,lseek会将errno设置为ESPIPE,然后返回-1.

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()
{
    
    
	int result = -1;	//创建管道结果
	
	result = pipe(fd);	//创建管道
	if(result == -1){
    
    
		printf("建立管道失败\n");	//打印信息
		return -1;
	}
	...		//正常程序处理过程
	...
}

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

pipe()函数的例子

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

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

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

        如图上图所示的模型,在子进程中可以向管道写入数据,而写入的数据可以从父进程中读出。其完整代码如下, 子进程中向管道写"Leo is handsome",父进程中读出这些信息。

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

int main()
{
    
    
        int result = -1;        //创建管道结果
        int fd[2],nbytes;       //文件描述符,字符个数
        pid_t pid;      //PID值
        char string[] = "Leo is handsome";
        char readbuffer[80];
        /* 文件描述符1用于写,文件描述符0用于读 */
        int *write_fd = &fd[1]; //写文件描述符
        int *read_fd = &fd[0];  //读文件描述符

        result = pipe(fd);      //建立管道
        if(result == -1)        //建立管道失败
        {
    
    
                printf("建立管道失败\n");       //打印信息
                return -1;      //返回错误结果
        }

        pid = fork();
        if(pid == -1)   //fork失败
        {
    
    
                printf("fork失败\n");   //打印信息
                return -1;      //返回错误结果
        }
        if(pid == 0)    //子进程
        {
    
    
                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;
}

在这里插入图片描述

管道阻塞和管道操作的原子性

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

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

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

管道操作原子性的代码

        例如,下面的代码为一个管道读写的例子。在成功建立管道后,子进程向管道中写入数据,父进程从管道中读出数据。子进程一次写入128个字节的数据,父进程每次读取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()
{
    
    
        int result = -1;        //创建管道结果
        int fd[2],nbytes;       //文件描述符,字符个数
        pid_t pid;      //PID值
        char string[WRITELEN] = "Leo is handsome";
        char readbuffer[10*K];  //读缓冲区
        /* 文件描述符1用于写,0用于读 */
        int *write_fd = &fd[1];
        int *read_fd = &fd[0];

        result = pipe(fd);      //建立管道
        if(result == -1)        //建立管道失败
        {
    
    
                printf("建立管道失败\n");
                return -1;
        }
        pid = fork();   //分叉程序
        if(pid == -1)   //fork失败
        {
    
    
                printf("fork进程失败\n");
                return -1;
        }
        if(pid == 0)    //子进程
        {
    
    
                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;
}



在这里插入图片描述
        可以发现,父进程每次读取10K字节的数据,读了13次将全部数据读出。最后一次读数据,由于缓冲区中只有6K字节的数据,所以仅仅读立6K字节。
        子进程一次性写入了128K字节的数据,当父进程将全部数据 读取 完毕的时候,子进程的write()函数才返回将写入信息(”写入131072个数据,剩余0个数据“)打印出来。
        上述操作证明管道的操作是阻塞性质的。

命名管道

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

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

创建FIFO

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

$mkfifo /ipc/namedfifo
$ls -l /ipc/namedfifo
prw-rw-r-- 1 linux-c linux-c 

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

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

FIFO操作

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

消息队列

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

消息缓冲区结构

        常用的结构是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个字节,这其中包括mtypee成员,他的长度是4个字节(long 类型)。

结构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
};

结构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;	//序列号
}

这个结构描述的主要是一些底层的东西,简单介绍如下:

  • key:key参数用于区分消息队列。
  • uid:消息队列用户的ID号
  • gid:消息队列用户组的ID号
  • cuid:消息队列创建者的ID号
  • cgid:消息队列创建者的组ID号
  • mode:权限,用户控制读写,例如0666,可以对消息进行读写操作
  • seq:序列号

内核中的消息队列关系

        作为IPC的消息队列,其消息的传递是通过Linux内核来进行的。如图所示的结构成员与用户空间的表述基本一致。在消息的发送和接收的时候,内核通过一个比较巧妙地设置来实现消息插入队列的动作和从消息中查找消息的算法。
在这里插入图片描述
        结构list_head形成的一个链表,而结构msg_msg之中的m_list成员是一个struct list_head类型的变量,通过此变量,消息形成了一个链表,在查找和插入时,对m_list域进行偏移操作就可以找多对应的消息体位置。内核中的代码在头文件<linux/msg.h>和<linux/msg.c>中,主要的实现是插入消息和取出消息的操作。

键值构建ftok()函数

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

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

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

$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");
}
...

获得消息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一起使用时,如果队列早已存在则将出错。
    如果只使用了IPC_CREAT,msgget()函数或者返回新创建消息队列的消息队列标识符,或者会发返回现有的具有同一个关键字的队列的标识符。如果同时使用了IPC_EXCL和IPC_CREAT,那么将可能会有两个结果:创建一个新的队列,如果该队列存在,则 调用将出错,并放回-1.IPC_EXCL本身没有什么用处的,但在与IPC_CREAT组合使用时,它可以用于保证没有一个现存的队列为了访问而被打开。例如,下面的代码创建一个消息队列:
...
key_t key;
int msg_flags,msg_id;
msg_flags = IPC_CREAT|IPC_EXCL;	//消息的标志为建立、可执行
msg_id = msgget(key, msg_flags|0x0666);	//建立消息
if(msg_id == -1)	//建立消息失败
{
    
    
	printf("消息建立失败\n");	//打印信息
	return 0;
}

发送消息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()获得的返回值。第2个参数是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_NOWATI;	//直接读取消息,不等待
msg_mbuf.mtype = 10;	//消息的大小为10字节
memcpy(msg_mbuf.mtext, "测试消息", sizeof("测试消息"));	//将数据复制如消息数据缓冲区

ret = msgsnd(msg_id, &msg_mbuf, sizeof("测试消息"), msg_sflags);	//向消息ID发送消息

if(ret == -1)	//发送消息失败
{
    
    
	printf("发送消息失败\n");	//打印消息
}

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

接收消息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 msgtype, int msgflg);
  • msgrcv()函数的第1个参数msqid是用来指定,在消息获取过程中所使用的队列(该值是由前面调用msgget()得到的返回值)。
  • 第2个参数msgp代表消息缓冲区变量的地址,获取的消息将存放在这里。
  • 第3个参数msgsz代表消息缓冲区结构的大小,不包括mtype成员的长度
  • 第4个参数mtype指定要从队列中获取的消息类型。内核将查找队列中具有匹配类型的第一个到达的消息,并把它复制返回到由msgp参数所指定的地址中。如果mtype参数传送到一个为0的值,则将返回队列中最老的消息,不管该消息的类型是什么。
            如果把IPC_NOWAIT作为一个标志传送给该函数,而队列中没有任何消息,则该次调用将会向调用进程返回ENOMSG,否则,调用进程将阻塞,直到满足msgrcv()参数的消息到达队列为止。如果在客户等待消息的时候队列被删除了,则返回EIDRM。如果在进程给你阻塞并等待消息的到来时捕捉到一个信号,则返回EINTR。函数msgrcv的使用代码如下:
msg_rflags = IPC_NOWAIT|MSG_NOERROR;	//消息接收标记
ret = msgrcv(msg_id, &msg_mbuf, 10, msg_rflags);	//接收信息
if(ret == -1)
{
    
    
	printf("接收信息失败\n");
}
else
{
    
    
	printf("接收信息成功,长度:%d\n",ret);
}

消息控制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:内核删除队列。使用此命令执行后,内核会把消息队列从系统中删除。

消息队列的一个例子

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

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

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

#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(ret == -1)
	{
    
    
		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\n",ctime(&(msg_info.msg_stime)));	//最后发送消息的时间
	
	printf("最后接受消息的时间:%s\n",ctime(&(msg_info.msg_rtime)));	//最后接受消息的时间

	printf("最后变化的时间:%s\n",ctime(&(msg_info.msg_ctime)));	//最后变化的时间

	printf("消息UID是:%d\n",msg_info.msg_perm.uid);	//消息的UID
	printf("消息UID是:%d\n",msg_info.msg_perm.gid);	//消息的GID


}

int main()
{
    
    
	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)
	{
    
    
		printf("成功建立KEY\n");
	}
	else
	{
    
    
		printf("建立KEY失败\n");
	}
	msg_flags = IPC_CREAT|IPC_EXCL;	//消息类型
	msg_id = msgget(key, msg_flags|0x0666);	//建立消息
	if(msg_id == -1)
	{
    
    
		printf("消息建立失败");
		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(ret == -1)
	{
    
    
		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(ret == -1)
	{
    
    
		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(ret == -1)
	{
    
    
		printf("设置消息属性失败\n");
		return 0;
	}
	msg_show_attr(msg_id, msg_info);	//显示消息属性

	ret = msgctl(msg_id, IPC_RMID, NULL);
	if(ret == -1)
	{
    
    
		printf("删除消息失败\n");
		return 0;
	}
	return 0;
}

信号量

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

信号量数据结构

        信号量数据结构是信号量程序设计中经常使用的数据结构。

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

        semget()函数用于创建一个新的信号量集合,或者访问现有的集合。其原型如下,其中第一个参数key是ftok生成的键值,第二个参数nsems参数可以指定在新的集合中应该创建的信号量的数目,第三个参数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_CREAT,semget()或者返回新创建的信号量集合的信号量标识符;或者返回早已存在的具有同一个关键字值的集合的标识符。如果同时使用IPC_EXCL和IPC_CREAT,那么将有两种可能的结果:如果结合不存在,则创建一个新的集合,如果集合早已存在,则调用失败,并返回-1。IPC_EXCL本身是没有什么用处的,但当与IPC_CREAT组合使用时,它可以用于防止为了访问而打开现有的信号量集合。
    利用semget()函数包装建立信号量的代码如下:
typedef int sem_t;
union semun{
    
    	//信号量操作的联合结构
	int val;	//整型变量
	struct semid_ds	*buf;	//semid_ds结构指针
	unsigned short *array;	//数组类型
}arg;
sem_t CreatSem(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(semid == -1)	//获得信号量失败
	{
    
    
		printf("creat semaphore error\n");	//打印信息
		return 0;
	}
	semctl(semid,0,SETVAL,sem);	//发送命令,建立value个初始值的信号量
	return semid;	//返回建立的信号量
}

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

信号量操作函数semop()

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

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

        semop()函数第二个参数sops是一个指针,指向将要在信号量集合上执行操作的一个数组,而第三个参数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_up为0,则将进程设置为睡眠状态,直到信号量的值为0为止。

        例如 "struct sembuf sem={0, +1. NOWAIT};"表示对信号量0,进行加1操作。用函数semop()可以构建基本的P、V操作,代码如下所示。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));	//发送信号量操作方法
}
控制信号量参数semctl函数

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

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

        函数semctl()用于在信号量集合上执行控制操作。这个调用类似于函数msgctl(),msgctl()函数是用于消息队列上的操作。semctl()函数的第一个参数是关键字的值(在我们的例子中它是调用semget()函数所返回的值)。第二个参数(semunm)是将要执行操作的信号量的编号,它是信号集合的一个索引值,对于集合中的第一个信号量(有可能只有这一个信号量)来说,它的索引值将是一个为0的值。cmd参数代表将要在集合上执行的命令。其取值如下所示。
在这里插入图片描述
参数arg代表类型semun的一个实例,这个特殊的联合体是在Linux/sem.h中定义的,如下所示;

  • val:当执行SETVAL命令时将用到这个成员,它用于指定要把信号量设置成什么值。
  • buf:在命令IPC_STAT/IPC-SET中使用,它代表内核中所使用的内部信号量数据结构的一个复制
  • array:用在GETALL/SETALL命令中的一个指针。它应当指向整数值的一个数组。在设置或获取集合中所有信号量的过程中,将会用到该数组。
  • 剩下的参数_buf和_pad将在内核中的信号量代码的内部使用。

利用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,sesm);		//获得信号量的值
}

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

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

	semctl(semid,0,IPC_RMID,sem);	//设置信号量
}
一个信号量操作的例子

        下面的代码先建立一个信号量,然后对这个信号量进行P,V操作,并将信号量的值打印出来,最后销毁信号量。

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

信号

        信号(signal)机制是UNIX系统中最为古老的进程之间的通信机制。它用于在一个或多个进程之间传递异步信号。信号可以有各种异步事件产生,例如键盘中断等。Shell也可以用信号将作业控制命令传递给它的子进程。
        Linux系统中定义了一系列的信号,这些信号可以由内核产生,也可以由系统中的其他进程产生,只要这些进程有足够的权限。可以使用kill命令(kill-l)在机器上列出所有的信号,如下所示:
在这里插入图片描述
        进程可以屏蔽掉大多数的信号,除了SIGSTOP和SIGKILL。SIGSTOP信号使一个正在运行的进程暂停,而信号SIGKILL则使正在运行的进程退出。进程可以选择系统的默认方式处理信号,也可以选择自己的方式处理产生信号。信号之间不存在相对的优先权,系统也无法处理同时产生的多种信号,也就是说,进程不能分辨它收到的是1个或者是42个SIGNAL信号。

在这里插入图片描述

在这里插入图片描述

信号截取函数signal()

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

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

        signal()函数的原型说明此函数要求两个参数,返回一个函数指针,而该指针所指向的函数无返回值(void).第一个参数signum是一个整型数,第二个参数是函数指针,它所指向的函数需要一个整型参数,无返回值。用一般语言来描述就是要向信号处理程序传送一个整型参数,而它却无返回值。当调用signal设置信号处理程序时 ,第二个参数是指向该函(也就是信号处理函数)的指针。signal的返回值指向以前信号处理程序的指针。
        如下代码截取了系统的信号SIGSTOP和SIGKILL,用命令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()
{
    
    
	sighandler_t ret;
	ret = signal(SIGSTOP, sig_handle);	//挂接SIGSTOP信号处理函数
	if(SIG_ERR == ret)	//挂接失败
	{
    
    
		printf("为SIGSTOP挂接信号处理函数失败\n");
		return -1;
	}

	ret = signal(SIGKILL, sig_handle);	//挂接SIGKILL处理函数
	if(SIG_ERR == ret)	//挂接失败
	{
    
    
		printf("为SIGKILL挂接信号处理函数失败\n");
		return -1;
	}
	for(;;);	//等待程序推出
}
向进程发送信号函数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。当pid为0时,向当前系统的所有进程发送信号sig,即“群发”的意思。raise()函数在当前进程中自举一个信号sig,即向当前进程发送信号。

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

共享内存

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

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

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

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

#include <stdio.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操作,结果再置为标志参数,这样映射的共享内存段只能标记为只读方式。
        当申请成功时,对共享内存的操作与一般内存一样,可以直接进行写入和读出,以及偏移的操作。

删除共享内存函数shmdt()

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

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

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

共享内存控制函数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);

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

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

一个共享内存的例子

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

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

static char msg[] = "Leo is handsome\n";
int main()
{
    
    
	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);	//挂接共享内存
		Sem_V(semid);	//减小信号量
		printf("共享内存的值为:%s\n",shmc);	//打印信息
		shmdt(shmc);	//摘除共享内存
	}
	return 0;
}

猜你喜欢

转载自blog.csdn.net/zouchengzhi1021/article/details/113247641