1.进程间通信

在 Linux 下的多个进程间的通信机制叫做 IPC,它是多个进程之间相互沟通的一种方

法。在 Linux 下有多种进程间通信的方法:1.半双工管道、FIFO(命名管道)2.消息队列3.信号

4.信号量5.共享内存6.套接字socket等。使用这些通信机制可以为 Linux 下的网络服务器开发提供灵活而又

坚固的框架

1. 半双工管道

管道是一种把两个进程之间的标准输入和标准输出连接起来的机制。管道是一种历史

悠久的进程间通信的办法,自 UNIX 操作系统诞生,管道就存在了。

由于管道仅仅是将某个进程的输出和另一个进程的输入相连接的单向通信的办法,因

此称其为“半双工”

在 shell 中管道用“|”表示

由于进程 A 和进程 B 都能够访问管道的两个描述符,因此管道创建完毕后要设置在各

个进程中的方向,希望数据向那个方向传输。这需要做好规划,两个进程都要做统一的设

置,在进程 A 中设置为读的管道描述符,在进程 B 中要设置为写;反之亦然,并且要把不

关心的管道端关掉。对管道的读写与一般的 IO 系统函数一致,使用 write()函数写入数据,

read()函数读出数据,某些特定的 IO 操作管道是不支持的,例如偏移函数 lseek()。

pipe()函数介绍

数组中的 filedes 是一个文件描述符的数组,用于保存管道返回的两个文件描述符。数

组中的第 1 个元素(下标为 0)是为了读操作而创建和打开的,而第 2 个元素(下标为 1)

是为了写操作而创建和打开的。直观地说,fd1 的输出是 fd0 的输入。当函数执行成功时,

返回 0;失败时返回值为–1。建立管道的代码如下:

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

当管道的写端没有关闭时,如果写请求的字节数目大于阈值 PIPE_BUF,写操作的返

回值是管道中目前的数据字节数。如果请求的字节数目不大于 PIPE_BUF,则返回管道中

现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,

管道中数据量不小于请求的数据量)。

管道进行写入操作的时候,当写入数据的数目小于 128K 时写入是非原子的,如果把

父进程中的两次写入字节数都改为 128K,可以发现:写入管道的数据量大于 128K 字节时,

缓冲区的数据将被连续地写入管道,直到数据全部写完为止,如果没有进程读数据,则一

直阻塞

意思就是:子进程给父进程写入100k字节数据,父进程每次读取10k字节的数据,父进程分10次读取完。

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

2.命名管道

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

在文件系统中命名管道是以设备特殊文件的形式存在的。

不同的进程可以通过命名管道共享数据。

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. 消息队列

为什么会需要消息队列(MQ)?

主要原因是由于在高并发环境下,由于来不及同步处理,请求往往会发生堵塞,比如说,大量的insert,update之类的请求同时到达MySQL,直接导致无数的行锁表锁,甚至最后请求会堆积过多,从而触发too many connections错误。通过使用消息队列,我们可以异步处理请求,从而缓解系统的压力。

消息队列是内核地址空间中的内部链表,通过 Linux 内核在各个进程之间传递内容。

消息顺序地发送到消息队列中,并以几种不同的方式从队列中获取,每个消息队列可以用

IPC 标识符唯一地进行标识。内核中的消息队列是通过 IPC 的标识符来区别的,不同的消

息队列之间是相对独立的。每个消息队列中的消息,又构成一个独立的链表

1.消息缓冲区结构

常用的结构是 msgbuf 结构。程序员可以以这个结构为模板定义自己的消息结构。在

头文件<linux/msg.h>中,它的定义如下:

struct msgbuf {

long mtype;

char mtext[1];

};

     结构 msgid_ds

     结构 ipc_perm

2. 结构 ipc_perm

作为 IPC 的消息队列,其消息的传递是通过 Linux 内核来进行的。如图 4.4 所示的结

构成员与用户空间的表述基本一致。在消息的发送和接收的时候,内核通过一个比较巧妙

的设置来实现消息插入队列的动作和从消息中查找消息的算法

结构 list_head 形成一个链表,而结构 msg_msg 之中的 m_list 成员是一个 struct list_head

类型的变量,通过此变量,消息形成了一个链表,在查找和插入时,对 m_list 域进行偏移

操作就可以找到对应的消息体位置。内核中的代码在头文件<linux/msg.h>和<linux/msg.c>

中,主要的实现是插入消息和取出消息的操作。

3.消息队列的实现

消息队列的实现包括获取键值创建和打开队列、添加消息、读取消息和控制消息队列这四种操作。

头文件:

            #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>

. ftok(): 

           作用:键值构建 

           原型: key_t ftok(const char *pathname, int proj_id);,返回键值key

.msgget:

           作用 : 创建和打开队列,其消息数量受系统限制。

           原型: int msgget(key_t key, int msgflg);,参数key是ftok()的返回值,返回值为 msg_id

.msgsnd:

           作用:添加消息,将消息添加到消息队列尾部。

           原型: int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);参数msqid 为                                msgget()的返回值。

.msgrcv:

           作用:读取消息,从消息队列中取走消息。

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

                        调用msgget()的返回值msqid。

.msgctl:

           作用:控制消息队列。

           原型: int msgctl(int msqid, int cmd, struct msqid_ds *buf);           

        消息队列的一个代码例子:

                        

4.信号量

    为什么要使用信号量

为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问 代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它, 也就是说信号量是用来调协进程对共享资源的访问的。其中共享内存的使用就要用到信号量

    信号量的工作原理

由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:

P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行

V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

举个例子,就是 两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为 当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件sys/sem.h中。

  • 新建信号量函数 semget()

  • 信号量操作函数 semop()

  • 控制信号量参数 semctl()函数

    

       一个信号量操作的例子

01 int main(void)

02 {

03 key_t key; /*信号量的键值*/

04 int semid; /*信号量的 ID*/

05 char i;

06 int value = 0;

07

08 key = ftok("/ipc/sem",'a'); /*建立信号量的键值*/

09

10 semid = CreateSem(key,100); /*建立信号量*/

11 for (i = 0;i <= 3;i++){ /*对信号量进行 3 次增减操作*/

12 Sem_P(semid); /*增加信号量*/

13 Sem_V(semid); /*减小信号量*/

14 }

15 value = GetvalueSem(semid); /*获得信号量的值*/

16 printf("信号量值为:%d\n",value); /*打印结果*/

17

18 DestroySem(semid); /*销毁信号量*/

19 return 0;

20 }

5. 共享内存

共享内存是在多个进程之间共享内存区域的一种进程间的通信方式,它是在多个进程

之间对内存段进行映射的方式实现内存共享的。这是 IPC 最快捷的方式,因为共享内存方

式的通信没有中间过程,而管道、消息队列等方式则是需要将数据通过中间机制进行转换;

与此相反,共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的

物理空间,仅仅是地址不同而已,因此不需要进行复制,可以直接使用此段空间。

  • 创建共享内存函数 shmget()

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

  • 删除共享内存函数 shmdt()

  • 共享内存控制函数 shmctl()

6.socket在后面讲到

猜你喜欢

转载自blog.csdn.net/weixin_40535588/article/details/89218106