正在阅读:

linux下IPC通信机制

676

一. 目的

总结linux下ipc通信机制和同步机制。

不同进程间的通信本质:

每个进程各自有不同的用户地址空间, 任何一个进程的全局变量在另一个进程中都看不到,

所以进程之间要交换数据必须通过内核, 在内核中开辟一块缓冲区, 进程A把数据从用户空间

拷到内核缓冲区, 进程B再从内核缓冲区把数据读走, 内核提供的这种机制称为进程间通信。

进程之间可以看到一份公共资源, 而提供这份资源的形式或者提供者不同,造成了通信方式不同.

二. 总结

1.  pipe 匿名管道

注:

PIPE_BUF在 include/linux/limits.h 中定义,不同的内核版本可能会有所不同.

Posix.1要求PIPE_BUF至少为512字节,red hat 7.2中为4096

#include <unistd.h>

int pipe (int fd[2]);

fd参数返回两个文件描述符, fd[0]指向管道的读端,fd[1]指向管道的写端.

(1) 父进程创建管道,得到两个⽂件描述符指向管道的两端

(2) 父进程fork出子进程,⼦进程也有两个⽂件描述符指向同⼀管道。

(3) 父进程关闭fd[0],子进程关闭fd[1],即⽗进程关闭管道读端,⼦进程关闭管道写端

(因为管道只支持单向通信)。⽗进程可以往管道⾥写,⼦进程可以从管道⾥读,管道是

⽤环形队列实现的,数据从写端流⼊从读端流出,这样就实现了进程间通信。

管道读取数据的四种的情况

(1)  读端不读,写端一直写

直到写满管道的最大容量,这个时候会阻塞,重新读了read后write操作才会返回.

向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,

写进程就会试图向管道写入数据.如果读进程不读走管道中的数据,那么写操作将一直阻塞.

对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN, 提醒以后再试.

(2)  写端不写,但是读端一直读

直到读完所有的数据,这个时候会阻塞, 重新write后 read操作才会返回.

(3)  读端一直读,且fd[0]保持打开,而写端写了一部分数据不写了,并且关闭fd[1]

当所有的数据被读取完毕后,再次read会返回0,就像读到文件结尾一样.

(4)  读端读了一部分数据,不读了且关闭fd[0],写端一直在写且f[1]还保持打开状态

子进程会收到SIGPIPE信号,一般会导致子进程异常退出.

注:

只有在管道的读端存在时,向管道中写入数据才有意义.

否则,向管道中写入数据的进程将收到内核传来的SIFPIPE信号,

应用程序可以处理该信号,也可以忽略(默认动作则是应用程序终止)

管道特点

(1)  管道只允许具有血缘关系的进程间通信,如父子进程间的通信。

(2)  管道只允许单向通信, 管道是半双工的,先进先出的.

(3)  管道内部保证同步机制,从而保证访问数据的一致性.

(4)  面向字节流

(5)  管道随进程,进程在管道在,进程消失管道对应的端口也关闭,两个进程都消失管道也消失.

管道例子

(1)  用于shell

管道可用于输入输出重定向,它将一个命令的输出直接定向到另一个命令的输入

(2)  用于具有亲缘关系的进程间通信

父进程通过管道发送一些命令给子进程,子进程解析命令,并根据命令作相应处理

管道局限性

管道的主要局限性正体现在它的特点上:

(1)  只支持单向数据流

(2)  只能用于具有亲缘关系的进程之间

(3)  没有名字

(4)  管道的缓冲区是有限的(管道制存在于内存中,在管道创建时, 为缓冲区分配一个页面大小)

(5)  管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,

比如多少字节算作一个消息(或命令、或记录)等等;

2.  popen 匿名管道

#include <stdio.h>

FILE* popen(const char *command, const char *open_mode);

int pclose(FILE *stream_to_close);

fread

popen()的实现方式及优缺点:

请求popen()调用运行一个程序时,它首先启动shell,即系统中的sh命令

,然后将command字符串作为一个参数传递给它.

优点是:

在Linux中所有的参数扩展都是由shell来完成的.

所以在启动程序(command中的命令程序)之前先启动shell来分析命令字符串,

也就可以使各种shell扩展(如通配符)在程序启动之前就全部完成,这可以通过

popen()启动非常复杂的shell命令.

而它的缺点就是:

对于每个popen()调用,不仅要启动一个被请求的程序,还要启动一个shell,

即popen()调用将启动两个进程, 从效率和资源的角度看. popen()函数的调用比正常方式要慢一些.

 

把管道用作标准输入和标准输出

介绍用管道来连接两个进程的更简洁方法,可以把fd设置为已知值,一般是标准输入输出0或1.

这样做最大的好处是可以调用标准程序,即那些不需要以文件描述符为参数的程序

#include <unistd.h>

int dup(int file_descriptor);

int dup2(int file_descriptor_one, int file_descriptor_two);

dup调用创建一个新的文件描述符与作为它的参数的那个已有文件描述符指向同一个文件或管道.

对于dup()函数而言,新的文件描述总是取最小的可用值.

而dup2()所创建的新fd或者与int fd2相同,或者是第一个大于该参数的可用值.

所以当我们首先关闭文件描述符0后调用dup(),那么新的文件描述符将是数字0.

 

3.  fifo

有名管道

#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

这里pathname是路径名,mode是sys/stat.h里面定义的创建文件的权限.

fifo管道是先调用mkfifo创建, 然后再用open打开得到fd来使用.

它是半双工的的,一般不能使用O_RDWR打开,而只能用只读或只写打开.

fifo可以用在非亲缘关系的进程间,而它的真正用途是在服务器和客户端之间.

由于它是半双工的所以,如果要进行客户端和服务器双方的通信的话,

每个方向都必须建立两个管道,一个用于读,一个用于写.

FIFO可以说是管道的推广,克服了管道无名字的限制,

使得无亲缘关系的进程同样可以采用先进先出的通信机制进行通信.

服务端:

客户端:

4.  tcp/udp socket

通用socket套接字使用

UDP数据包套接字:

 socket()、bind()、sendto()、recvfrom()、close()

TCP流套接字:

 socket()、bind()、listen()、accept()、connect()、read()、write()、close()

5.  broadcast socket

6.  muticast socket

7.  unix domain socket

服务端:

客户端:

8.  netlink socket

Netlink套接字是用以实现用户进程与内核进程通信的一种特殊的进程间通信(IPC).

也是网络应用程序与内核通信的最常用的接口.

(1)  NETLINK_ROUTE:用户空间路由damon,如BGP,OSPF,RIP和内核包转发模块的通信信道

用户空间路由damon通过此种netlink协议类型更新内核路由表

(2)  NETLINK_FIREWALL:接收IPv4防火墙代码发送的包

(3)  NETLINK_NFLOG:用户空间iptable管理工具和内核空间Netfilter模块的通信信道

(4)  NETLINK_ARPD:用户空间管理arp表

(5)  NETLINK_USERSOCK:用户态socket协议

(6)  NETLINK_NETFILTER:netfilter子系统

(7)  NETLINK_KOBJECT_UEVENT:内核事件向用户态通知

(8)  NETLINK_GENERIC:通用netlink

Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,

特点:

(1)  用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,

内核态需要使用专门的内核 API 来使用 netlink

用户态应用使用标准的 socket API有sendto(), recvfrom(),sendmsg(), recvmsg()

(2)  netlink是一种异步通信机制, 在内核与用户态应用之间传递的消息保存在socket

缓存队列中,发送消息只是把消息保存在接收者的socket的接收队列,

而不需要等待接收者收到消息, 它提供了一个socket队列来平滑突发的信息

(3)  使用 netlink 的内核部分可以采用模块的方式实现, 使用 netlink 的应用部分和

内核部分没有编译时依赖

(4)  netlink 支持多播, 内核模块或应用可以把消息多播给一个netlink组,

属于该neilink 组的任何内核模块或应用都能接收到该消息,

内核事件向用户态的通知机制就使用了这一特性(热插拔事件)

(5)  内核可以使用 netlink 首先发起会话

(6)  netlink采用自己独立的地址编码, struct sockaddr_nl;

(7)  每个通过netlink发出的消息都必须附带一个netlink自己的消息头,struct nlmsghdr

 

在基于netlink的通信中,有两种可能的情形会导致消息丢失:

(1) 内存耗尽,没有足够多的内存分配给消息

(2) 缓存复写,接收队列中没有空间存储消息,这在内核空间和用户空间之间通信

时可能会发生缓存复写在以下情况很可能会发生:

(3)  内核子系统以一个恒定的速度发送netlink消息,但是用户态监听者处理过慢

(4) 用户存储消息的空间过小

如果netlink传送消息失败,那么recvmsg()函数会返回No buffer spaceavailable(ENOBUFS)错误

9.  signal_fd

三种新的fd加入linux内核的的版本:

signalfd:2.6.22

timerfd:2.6.25

eventfd:2.6.22

三种fd的意义:

lsignalfd

传统的处理信号的方式是注册信号处理函数;由于信号是异步发生的,

要解决数据的并发访问,可重入问题。

signalfd可以将信号抽象为一个文件描述符,当有信号发生时可以对其read,

这样可以将信号的监听放到select、poll、epoll等监听队列中.

ltimerfd

可以实现定时器的功能,将定时器抽象为文件描述符,当定时器到期时可以对其read,

这样也可以放到监听队列的主循环中。

leventfd

实现了线程之间事件通知的方式,也可以用于用户态和内核通信。

eventfd的缓冲区大小是sizeof(uint64_t);向其write可以递增这个计数器,

read操作可以读取,并进行清零;eventfd也可以放到监听队列中,

当计数器不是0时,有可读事件发生,可以进行读取。

三种新的fd都可以进行监听,当有事件触发时,有可读事件发生。

 

#include <sys/signalfd.h>

int signalfd(int fd, const sigset_t*mask, int flags);

从 Linux 2.6.27 开始, 可以经过位或运算放置在 flags 里以来改变signalfd() 的行为:

SFD_NONBLOCK/SFD_CLOEXEC

signalfd() 创建一个可以用于接受以调用者为目标的信号的文件描述符.

这提供了一个使用信号处理器或sigwaitinfo(2)的改良方式,并且这种

方式存在一个优点就是可以使用select(2)、poll(2) 和epoll(7) 来监视.

10.  time_fd

#include <sys/timerfd.h>

int timerfd_create(int clockid, int flags);

int timerfd_settime(int fd, int flags, const struct itimerspec *new_value,

struct itimerspec *old_value);

int timerfd_gettime(int fd, struct itimerspec *curr_value);

创建定时器fd,CLOCK_REALTIME: 真实时间类型,修改时钟会影响定时器;

CLOCK_MONOTONIC:相对时间类型,修改时钟不影响定时器

阻塞等待定时器到期。返回值是未处理的到期次数.

比如定时间隔为2秒,但过了10秒才去读取,则读取的值是5.

注意:  编译时要加rt库

11.  event_fd

eventfd在linux中是一个较新的进程通信方式,和信号量等不同的是

event不仅可以用于进程间的通信,还可以用户内核发信号给用户层的进程.

eventfd在virtIO后端驱动vHost的实现中作为vhost和KVM交互的媒介,起到了重大作用.

eventfd应该归结于低级通信行列,即不适用于传递大量数据,仅仅用于通知或者同步操作,

还要注意的是,该文件描述符并不对应固定的磁盘文件,故类似于无名管道,

这里也仅仅用于有亲缘关系之间的进程通信.

 

eventfd()创建了一个“eventfd对象”, 通过它能够实现用户态程序间

(我觉得这里主要指线程而非进程)的等待/通知机制,以及内核态向用户态通知的机制

此对象包含了一个被内核所维护的计数(uint64_t), 初始值由initval来决定。

#include <sys/eventfd.h>

int eventfd(unsigned int initval, int flags);创建一个eventfd文件描述符

int eventfd_read(int fd, eventfd_t *value); 向eventfd中写入一个值

int eventfd_write(int fd, eventfd_t value); 从eventfd中读出一个值

也可以通过标准的read和write函数来读取.

eventfd可以被epoll监控, 一旦有状态变化,可以触发通知.

被epoll监控的eventfd,如果在子线程中被多次写入,在主线程中是怎么读的?

在read前,如果eventfd被写多次,在read的时候也是一次全部读出.

通过测试发现, 如果eventfd在创建的时候传入EFD_SEMAPHORE 标志,

则会每次在eventfd_read的时候只减一,并不是把值一次性全部读出.

 

注:eventfd中的SEMAPHORE标志用法

* If EFD_SEMAPHORE was not specified and the eventfd counter
has a nonzero value, then a read(2) returns 8 bytes contain‐
ing that value, and the counter's value is reset to zero.

* If EFD_SEMAPHORE was specified and the eventfd counter has a
nonzero value, then a read(2) returns 8 bytes containing the
value 1, and the counter's value is decremented by 1.

示例: iEvtfd = eventfd(0,EFD_SEMAPHORE);

12. mmap

匿名内存映射

映射文件

13.  System-V 消息队列 MessageQuene

消息队列介绍:

消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法.

我们可以通过发送消息来避免命名管道的同步和阻塞问题.

但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。

Linux用宏MSGMAX和MSGMNB来限制一条消息的最大长度和一个队列的最大长度.

创建和访问一个消息队列

int msgget(key_t, key, int msgflg);

把消息添加到消息队列中

int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);

从一个消息队列获取消息

int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);

控制消息队列,它与共享内存的shmctl函数相似.

int msgctl(int msgid, int command, struct msgid_ds *buf);

command是将要采取的动作,它可以取3个值,

IPC_STAT: 把msgid_ds设置为消息队列的当前关联值,即用当前关联值覆盖msgid_ds的值.

IPC_SET: 如果进程有足够的权限, 就把消息列队的当前关联值设置为msgid_ds结构中给出的值

IPC_RMID: 删除消息队列

#define MSG_FILE "/tmp" //a pathname for generating a unique key

#define BUFFER 512 //set the buffer size to 255 bytes

#define PERM S_IRUSR|S_IWUSR //allow the user to read and write

key_t key;//发送和接收约定的唯一通信标识

key=ftok(MSG_FILE, BUFFER))  //利用ftok产生一个唯一的key,参数为路径和缓冲区大小

msgid=msgget(key, PERM | IPC_CREAT))

消息队列与命名管道的比较:

通过与命名管道一样,消息队列进行通信的进程可以是不相关的进程,

同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,

发送数据用write(),接收数据用read(),则在消息队列中,

发送数据用msgsnd(),接收数据用msgrcv().

而且它们对每个数据都有一个最大长度的限制.

与命名管道相比,消息队列的优势在于:

(1) 消息队列也可以独立于发送和接收进程, 消除了在同步命名管道的打开和关闭时可能产生的困难.

(2) 同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法.

(3) 接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收.

14.  System-V 信号量

信号量介绍:

注意请不要把它与之前所说的信号混淆起来,信号与信号量是不同的两种事物

为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,

它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区。

临界区域是指执行数据更新的代码需要独占式地执行.

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待

(即P(信号变量)) 和 发送(即V(信号变量))信息操作.

最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号.

而可以取多个正整数的信号量被称为通用信号量。这里主要讨论二进制信号.

信号量原理:

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

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

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

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,

可以进入临界区,使sv减1. 而第二个进程将被阻止进入临界区, 因为当它试图执行P(sv)时,sv为0,

它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,第二个进程可以恢复执行.

信号量使用:

它们声明在头文件sys/sem.h中

创建一个新信号量或取得一个已有信号量

int semget(key_t key, int num_sems, int sem_flags);

改变信号量的值

int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);

直接控制信号量信息

int semctl(int sem_id, int sem_num, int command, ...);

15. System-V 共享内存

共享内存介绍:

共享内存就是允许两个不相关的进程访问同一个逻辑内存.

共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式.

注意:

共享内存并未提供同步机制,两个进程可能同时读写。所以我们通常需要用

其他的机制来同步对共享内存的访问,例如前面说到的信号量。

共享内存使用:

创建共享内存

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

启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间

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

将共享内存从当前进程中分离

int shmdt(const void *shmaddr);

控制共享内存

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

 

优点:

非常方便,而且函数的接口也简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,

也加快了程序的效率. 同时,它也不像匿名管道那样要求通信的进程有一定的父子关系.

缺点:

共享内存没有提供同步的机制,往往要借助其他的手段来进行进程间的同步工作.

16.  POSIX 消息队列 MessageQuene

struct mq_attr attr;

attr.mq_maxmsg = 500

attr.mq_msgsize = 200

int flags = O_RDWR | O_CREAT | O_EXCL | O_NONBLOCK;

mqd_t mqd = mq_open(name, flags, FILE_MODE, (attr.mq_maxmsg != 0)? &attr:NULL);

mq_close(mqd);

/*关闭消息队列返回的描述符,但并没有释放资源,资源还在内核中*/

mq_unlink(name);/*释放掉内核占用的资源*/

mq_getattr(mqd, &attr);/*获取消息队列的属性*/

struct sigevent sigev;

sigev.sigev_notify = SIGEV_THREAD;

sigev.sigev_value.sival_ptr = NULL;

sigev.sigev_notify_function = notify_thread;

sigev.sigev_notify_attributes = NULL;

mq_notify(mqd, &sigev);

mq_send(mqd, ptr, len, prio);/*向消息队列中发送消息*/

char *buff = malloc(attr.mq_msgsize);

int n = mq_receive(mqd, buff, attr.mq_msgsize, NULL))

long om = Sysconf(_SC_MQ_OPEN_MAX),

long pm = Sysconf(_SC_MQ_PRIO_MAX));

17.  POSIX信号量

semaphore 的这种信号量不仅可用于同一进程的线程同步,也可以用于不同进程间同步

#include <semaphore.h>

无名/内存信号量:

int sem_init(sem_t *sem, int pshared, unsigned int value);

int sem_destroy(sem_t *sem);

int sem_wait(sem_t *sem);

等待信号量,如果信号量的值大于0,将信号量的值减1,立即返回.

如果信号量的值为0,则线程阻塞。相当于P操作, 成功返回0,失败返回-1

int sem_post(sem_t *sem);

释放信号量,让信号量的值加1, 相当于V操作.

 

有名信号量:

sem_t *sem sem_open(const char *name, int oflag, .../*mode_t mode,unsinged int value) ;

当打开一个一定存在的有名信号量时,ofalg设置为0.

int  flags = O_RDWR | O_CREAT | O_EXCL

unsigned int value =1;

sem = sem_open(argv[optind], flags, FILE_MODE, value);

int sem_close(sem_t *sem):关闭有名信号量

一个进程终止时,内核对其上仍打开的所有有名信号量自动执行关闭操作

关闭一个信号量并没有将他从系统中删除.

POSIX 有名信号量是随内核持续的:即使当前没有进程打开着某个信号量, 他的值仍保持.

int sem_unlink(const char *name):从系统中删除有名信号量

sem_getvalue(sem_t *sem;, int *val);

 

有名信号量sem_open和内存信号量sem_init创建信号量的区别:

(1)  创建有名信号量必须指定一个与信号量相关链的文件名称.

这个name通常是文件系统中的某个文件, 基于内存的信号量不需要指定名称

(2)  有名信号量sem 是由sem_open分配内存并初始化成value值

基于内存的信号量是由应用程序分配内存,有sem_init初始化成为value值.

如果shared为1,则分配的信号量应该在共享内存中.

(3)  sem_open不需要类似shared的参数

因为有名信号量总是可以在不同进程间共享的, 而基于内存的信号量通过

shared参数来决定是进程内还是进程间共享,并且必须指定相应的内存

(4)  基于内存的信号量不使用任何类似于O_CREAT标志的东西

也就是说,sem_init总是初始化信号量的值,因此,对于一个给定的信号量,

我们必须小心保证只调用sem_init一次,对于一个已经初始化过的信号量调用

sem_init,结果是未定义的。

(5)  内存信号量通过sem_destroy删除信号量,有名信号量通过sem_unlink删除.

18.  POSIX共享内存

创建:

char *name;

off_t length;

int flags = O_RDWR | O_CREAT | O_EXCL;

int fd = shm_open(name, flags, FILE_MODE);

char *ptr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
读写:

fd = shm_open(argv[1], O_RDONLY, FILE_MODE);

fd = shm_open(argv[1], O_RDWR, FILE_MODE);/*打开指定的共享内存区对象*/

struct stat stat;

fstat(fd, &stat);

ptr = mmap(NULL, stat.st_size, PROT_READ, MAP_SHARED, fd, 0);

ptr = mmap(NULL, stat.st_size, PROT_WRITE,MAP_SHARED, fd, 0);

19. Signal 信号

signal信号处理

sigaction

#include <signal.h>

int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);

发送信号

(1)  kill()函数

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid, int sig);

失败的原因科可能有:

给定的信号无效(errno = EINVAL)

发送权限不够( errno = EPERM )root权限

目标进程不存在( errno = ESRCH )

(1)  alarm()函数

进程可以调用alarm()函数在经过预定时间后向发送一个SIGALRM信号

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

20. Mutex

信号(signal)和信号量(semaphore)本质上并不算是进程间通信方式,

应该是进程间同步的方式,但是也可以起到一定的通信作用,故也列在上面。

pthread_mutex_t

pthread_cond_t

21. Lock

struct flock lock;

lock.l_type = F_WRLCK; //F_UNLCK

lock.l_whence = SEEK_SET;

lock.l_start = 0;

lock.l_len = 0;

fcntl(fd, F_SETLKW, &lock); //F_SETLK

22. Process-Waitpid

目前有:0条访客评论,博主回复1

  1. admin
    2018-08-12 16:52

    欢迎补充不同的方法和观点。

留下脚印,证明你来过。

*

*

流汗坏笑撇嘴大兵流泪发呆抠鼻吓到偷笑得意呲牙亲亲疑问调皮可爱白眼难过愤怒惊讶鼓掌
关闭