Linux 进程通信 和 IO模型

来源:互联网 发布:孔浩 java百度云 编辑:程序博客网 时间:2024/04/27 22:17

主要内容: 
第一部分 信号 
第二部分 PIPE 和 FIFO 
第三部分 消息队列 
第四部分 信号量 
第五部分 共享内存 
第六部分 I/O模型 
  
概念: 
在先了解Linux进程间通讯时,需要首先了解几个概念: 
1)随进程持续:IPC一直存在,直到打开IPC的最后一个进程关闭 
2)随内核持续:IPC一直存在,直到系统重启或删除该对象为止 
3)随文件系统持续:IPC一直存在,直到删除该对象为止。

  
第一部分 信号
  
Linux 信号: 
分类: 
(1)可靠信号与不可靠信号 或实时信号与非实时信号 
(2)不可靠信号信号值小于SIGRTMIN 
(3)非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。 
(4)Kill –l 查看系统支持的所有信号,信号在进程的生命周期有意义 
信号的处理方式: 
(1)忽略信号 
(2)捕获信号:当信号发生时,执行对应的信号处理函数 
(3)执行缺省操作:前32种信号每种都有各自的缺省操作函数,后32种信号缺省操作就是进程终止。 
(4) SIGKILL和SIGSTOP不能被忽略也不能被捕获 
  
信号的发送:kill()、raise()、 sigqueue()、alarm()、setitimer()、abort(). 
(1) int kill (pid_t pid,int signo) 发送信号到任何一个进程或进程组 
(2) Int raise (int signo) 发送信号到进程本身 
(3) Int sigqueue (pid_t pid, int sig, const union sigval val)发送信号到任何进程,支持信号带有参数。 
注:sigqueue()发送非实时信号时,第三个参数包含的信息仍然能够传递给信号处理函数; 但是不支持排队。 
信号的安装:signal()、sigaction() 
(1) void (*signal (int signum, void (*handler)) (int))) (int) 
    typedef void (*sighandler_t)(int) 
    sighandler_t signal(int signum, sighandler_t handler)) 
(2) sigaction() 
    int sigaction (int signum,const struct sigaction *act,struct sigaction *oldact)); 
信号集操作函数和信号阻塞未决函数(略) 
  
使用信号注意事项: 
1)防止不该丢失的信号丢失。不可靠信号只注册一次 
2) 程序的可移植性:尽量遵循POSIX 
3)程序的安全性:信号处理函数需要使用可重入函数 
 a. 不应该调用使用了静态变量的库函数 
 b. 不要调用malloc和free 
 c. 不要调用标准IO函数 
 d. 进入处理函数时,首先要保存errno,返回时再恢复。 
 e. longjmp和siglongjmp不能保证安全。 
  
4) 信号对系统调用的影响:可以中断系统调用。 
again: 
    if ((n = read (fd, buf, BUFFSIZE)) < 0) 
    { 
        if (errno == EINTR) goto again; 
    } 
  
sleep 和 alarm: 
1)在某些系统中sleep是通过alarm 实现的,如果两个交叉使用会得不到想要的结果。 
2)可以使用nanosleep 代替sleep   
sigsetjmp和 siglongjmp : 
  如果信号处理函数不可避免的要使用setjmp或longjmp需要用sigsetjmp和siglongjmp 代替 
  
第二部分 PIPE 和 FIFO
  
PIPE 
函数:int pipe (int fd[2]) 
特点: 
(1)管道是半双工的,双工通信时,需要建立起两个管道 
(2)只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程) 
(3)数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加到管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。 
(4)支持阻塞、非阻塞、异步IO 
注意事项: 
(1)当写端不存在的时候,读取管道返回0,否则系统调用阻塞直到有数据返回,返回的数据小于等于要读取的字节数 
(2)当读端不存在的时候,写管道将会产生SIGPIPE信号,当写小于等于PIPE_BUF大小的数据时保证是原子操作,当大于PIPE_BUF的时候,不保证原子操作。(man 7 pipe)(POSIX.1中规定PIPE_BUF不小于512,Linux下一般为4096,Linux Pipe中最大容量65536字节) 
(3)移植性:在某些系统中实现了双工管道,但是为了移植性不建议使用 

Linux IPC 和 IO模型 - shenze60 - linux博客

   
有名管道(FIFO) 
int mkfifo (const char * pathname, mode_t mode) 
第一个参数是一个普通的路径名,即创建后FIFO的名字。路径名只能位于本地文件系统中,并且如果路径名是一个已经存在的路径名时,会返回EEXIST错误。 
第二个参数是打开模式。 
打开规则(open): 
(1)读打开:阻塞直到有进程为写而打开 
(2)写打开:阻塞直到有进程为读而打开 
读写规则类似于pipe() 

Linux IPC 和 IO模型 - shenze60 - linux博客
    
PIPE或FIFO汇总(属于随进程持续IPC) 

当前操作
现存操作
阻塞IO
非阻塞IO
读打开FIFO
写打开
返回OK
返回OK
没有写打开
阻塞直到写打开
返回OK
写打开FIFO
读打开
返回OK
返回OK
没有读打开
阻塞直到打开
返回ENXIO错误
读空pipe或FIFO
pipe或FIFO 写打开
阻塞直到pipe 或FIFO中存在数据或者关闭写
返回EAGAIN
pipe或FIFO 没有写打开
返回0(文件结束)
返回0(文件结束)
写pipe或FIFO
pipe或FIFO读打开
--PIPE_BUF 
--PIPE_BUF 
pipe或FIFO没有打开
SIGPIPE
SIGPIPE

   OPEN_MAX: 最大描述符数, getconf OPEN_MAX  ulimit  (bsh/ksh)/ limit (csh)

  
第三部分 消息队列 
  
消息队列: 
目前系统中有两种消息队列:POSIX消息队列和系统V消息队列,目前系统V消息队列被广泛使用,但是考虑到可移植性,新开发程序尽量用POSIX消息队列。 
不同点: 
 (1)当读取消息时,POSIX总是返回最早的最高优先级消息,而系统V 可以返回任何类型的消息 
 (2)当一个消息放到空队列中POSIX消息队列可以产生信号或者新线程,而系统V没有提供类似的支持。 
相同点:它们都是随内核持续的,只有重启系统或者调用删除函数才能从系统中删除消息队列 
  
POSIX消息队列 
(1)消息队列可以看作一个单向链表 
(2)消息头中含有队列的两个属性:队列允许的最大消息数量和一个消息的最大大小 
(3)消息的长度可以为0,这样就没有了数据项 

Linux IPC 和 IO模型 - shenze60 - linux博客

   
基本操作函数: 
mq_open: 创建或打开一个现存的消息队列。 
mq_close:关闭一个消息队列 
mq_unlink:删除一个消息队列,只有在引用计数为0时,才会真正的删除消息队列。 
mq_getattr/mq_setattr:读取或设置消息队列属性。 
  
消息队列属性: 
struct mq_attr 

    long mq_flags:队列标志, 0或O_NONBLOCK 
    long mq_maxmsg:队列中运行的最大消息数 
    long mq_msgsize:队列中消息的最大大小 
    long mq_curmsgs:当前消息队列中消息的数量 

  
基本操作函数: 
发送消息: 
Int mq_send(mqd_t mqds, const char* ptr, size_t len, unsigned int prio); 
接收消息: 
ssize_t mq_send(mqd_t mqds, const char* ptr, size_t len, unsigned int* priop); 
  
Limits: 
MQ_OPEN_MAX:进程可以打开的最大消息队列数 
MQ_PRIO_MAX: 最大优先级加1的值,POSIX要求不小于32 
另外: 
mq_maxmsg: 受HARD_MAX限制(x86是32768) 
mq_msgsize:受INT_MAX限制(x86是2147483647) 
  
其它特性: 
(1)mq_notify: 实现信号或线程的异步调用。也可以通过AIO实现 
(2)支持mmap 

  
系统V消息队列 

Linux IPC 和 IO模型 - shenze60 - linux博客
 

   
struct msqid_ds 

    struct ipc_perm msg_perm; /* 权限信息 */ 
    msgqnum_t msg_qnum; /* 队列中的消息数目 */ 
    msglen_t msg_qbytes; /* 队列中允许的最大字节数 */ 
    pid_t msg_lspid; /* 上次发送的进程id */ 
    pid_t msg_lrpid; /* 上次接收的进程id */ 
    time_t msg_stime; /* 上次发送的时间 */    
    time_t msg_rtime; /* 上次接收的时间 */ 
    time_t msg_ctime; /* 上次更改时间 */ 
    . . . 
     }; 
  
创建或获得消息队列: 
int msgget (key_t key, int flag); 
其中key可以为IPC_PRIVATE 或有下面函数生成。 
key_t ftok (char*pathname, char proj); 
发送消息: 
int msgsnd (int msqid, const void *ptr, size_t nbytes, int flag); 
消息体格式如下: 
struct msgbuf 

    long mtype; //消息类型 
    long mtext[];//消息数据 由用户自由定义 

其中:消息类型必须大于0 
如果flag不是IPC_NOWAIT 时,由可能会发生阻塞。 
阻塞条件: 
1) 队列中的消息字节数已经达到限制(msg_qbytes) 
2) 消息的个数已经达到限制。 
当满足以下条件时,将不再阻塞: 
1)足够的空间存在 
2)消息队列已经被删除,返回EIDRM错误 
3)被信号中断,返回EINTR 
  
接收消息: 
ssize_t msgrcv (int msqid, void *ptr, size_t nbytes  , long type, int flag); 
接收消息说明: 
Flag: 
IPC_NOWAIT 如果没有满足条件的消息,调用立即返回,errno=ENOMSG 
IPC_EXCEPT 与type >0配合使用,返回队列中第一个类型不为type的消息 
IPC_NOERROR 如果队列中满足条件的消息内容大于所请求的nbytes字节,则把该消息截断,截断部分将丢失。 
消息类型: 
type ==0: 队列中第一个消息被返回 
type > 0: 返回类型等于type的消息 
type < 0: 返回类型小于等于type绝对值的消息(首先返回最低类型) 
如果flag不是IPC_NOWAIT 时,可能会被阻塞。如果设置了IPC_NOWAIT则返回ENOMSG 
阻塞返回的条件: 
1)队列中存在满足条件的消息 
2)消息队列已经被删除,返回EIDRM 
2)被信号中断, 返回EINTR 
控制: 
int msgctl (int msqid, int cmd, struct msqid_ds *buf );  可用户获取队列状态、更改队列参数已经删除队列,注意:只有创建者和超级用户才可以更改参数或删除队列。 
Limit: 
MSGMAX: 一个消息的最大字节数(8192) 
MSGMNB: 一个消息队列的最大字节数(16384) 

  
第四部分 信号量
  
信号量(semaphore): 
Linux下共有三种类型的信号量 
1) POSIX 基于文件系统的有名信号量 
2) POSIX基于内存的无名信号量 
3) System V信号量(内核中实现) 

Linux IPC 和 IO模型 - shenze60 - linux博客
 
  
有名信号量: 
1)具有内核持续性 
2)可以在无亲属关系的进程之间使用。 
  
有名信号量的基本操作函数: 
1) 打开或创建信号量(Linux创建的信号量可以在/dev/sem/下找到,有些系统会不一样) 
sem_t *sem_open(const char *name, int oflag, /*mode_t mode, unsigned int value*/); 
2)关闭信号量(程序退出会自动关闭) 
int sem_close(sem_t *sem) 
3)删除信号量(引用计数为0时,才真正删除) 
Int sem_ulink(const char* name); 
4)获得信号量 
int sem_wait(sem_t *sem); 
Int sem_trywait(sem_t *sem); 
5)释放信号量 
int sem_post(sem_t *sem); 
   
无名信号量: 
1) 用在亲属关系的进程或线程间 
2) 有的地方说是内核持续性,实际上一直存在知道用到的共享内存消失。对于多线程就是进程结束,对于多进程就是所有进程结束 
初始化和创建函数: 
int sem_init(sem_t *sem, int shared, unsigned int value); 
参数shared为0表示在不同进程间使用,为1表示用在线程间。 
Int sem_destroy(sem_t *sem); 
  
Limits: 
SEM_NSEMS_MAX :一个进程可以打开的最大信号量的数目,POSIX规定不小于256 
SEM_VALUE_MAX:信号量的最大值,POSIX规定不小于32767 
  
系统V信号量: 
1)具有内核持续性 
2)可以在无亲属关系的进程之间使用。 
3)每一个信号量都具有一个信号集。 

   
struct sem 

ushort semval; //信号量的值,非负数 
short sempid;   //上次成功访问sem_op的PID 
ushort_t semncnt;//等待semval>当前值的数量 
Ushort_t semzcnt://等待semval为0的数量 

Linux IPC 和 IO模型 - shenze60 - linux博客

 

   
基本操作函数: 
1)创建或打开信号量 
int semget (key_t key, int nsems, int oflag); 
此函数对sem_otime 赋值0,对sem_nsems 赋值nsems,而具体的信号集并没有初始化。需要调用semctl的SETVAL或SETALL命令进行初始化。 
注意:由于创建并初始化分两步执行,不能保证原子操作,所以非创建进程需要判断sem_otime 是否为0来判断信号量是否被初始化。只有初始化后才可以调用信号量操作函数 
2)信号量操作函数 
Int semop (int semid, struct sembuf* opsptr, size_t nops); 
其中 
struct sembuf 
 { 
unsigned short sem_num; /* set 中成员(0, 1, ..., nsems-1) */ 
short sem_op; /* 具体操作 (负数, 0, 或正数) */ 
short sem_flg; /* IPC_NOWAIT, SEM_UNDO */ 
}; 
nops:为opsptr数组的大小 

  
说明: 
a. SEM_UNDO:如果设置了此标志,那么进程结束的时候,操作将被取消。也就是说,进程如果没有释放资源就退出,那么内核将将释放。 
b. sem_op 大于0表示要释放sem_op数目的资源, sem_op等于0可以用于资源是否用完的测试,小于0表示要获得负的sem_op数目的资源. 
c. semop保证了对多个资源操作的原子性。 
3)信号量控制或删除函数。 
 int semctl (int semid,int semnum,int cmd,union semun arg) 
该调用实现各种控制操作, 
参数cmd指定具体的操作类型; 
参数semnum指定信号量SET成员的索引操作, 
参数arg用于设置或返回信号集信息。 
  
Limits: 
SEMMNS:系统中最大SEMID的个数。 
SEMMSL:每个SEMID中信号量的最大数目。 
SEMMNI: 每个信号量的SET的最大数目 
  
第五部分 共享内存 
  
共享内存-最快的进程间通讯方式 
共分三部分介绍 
1) mmap介绍 
2) POSIX共享内存 
3) SYSTEM V共享内存 

   
mmap介绍 
主要用于一下几种情况: 
1) 为常规文件提供内存映射的IO访问。 
2) 提供匿名内存映射 
3) POSIX共享内存 
主要操作函数如下: 
void *mmap (void* addr, size_t len, int prot, int flags, int fd, off_t offset); 
addr: 内存开始映射地址,通常是NULL表示让内核选择,addr 需要按照系统页对齐。 
len: 内存映射的大小或长度 
fd:要映射文件的句柄 
offset:文件的偏移, len、fd、offset指定了映射的文件大小和偏移 
prot:取PROT_READ, PROT_WRITE, PROT_EXEC, PROT_NONE 
flags: 一般取MAP_SHARED, MAP_PRIVATE, MAP_FIXED。其中MAP_FIXED一般不使用,和addr连用表示如果地址不可用则返回失败。MAP_ANON表示匿名映射此时fd取-1,仅仅是内存映射而不涉及具体文件操作,可用于亲属关系的进程间通讯。

 

  
int munmap (void *addr, size_t len); 删除映射,对于常规文件,会把修改保存到文件中。而对于MAP_PRIVATE标志的常规文件映射,会把修改丢掉不保存到文件。 
int msync (void *addr, size_t len, int flags) 把 MAP_SHARED映射的常规文件的修改保存到文件中。 
flags:取MS_ASYNC, MS_SYNC, MS_INVALIDATE 
   
常规文件映射说明见下图: 
文件大小为5000字节,映射大小为15000字节, 系统页大小为4096字节。

Linux IPC 和 IO模型 - shenze60 - linux博客

   
其它说明: 
mlock 或mlockall 使内存不被交换出去,常驻内存。 
munlock或munlockall 是对应的解锁函数。 
得到系统分页大小: 
getpagesize()或sysconf(_SC_PAGESIZE) 

  
POSIX共享内存 
基本操作函数 
创建或打开共享内存 
int shm_open (const char* name, int oflag, mode_t mode); 
删除共享内存 
int shm_unlink (const char *name); 
改变文件或内存对象的大小,(刚创建的共享内存为0) 
int ftruncate (int fd, off_t lenth); 
注意:对于常规文件,由于标准中改变文件大小没有对内容进行定义,所有不建议使用。 
获得共享内存信息: 
int fstat (int fd, struct stat *buf); 
  
共享内存和文件映射的异同: 

Linux IPC 和 IO模型 - shenze60 - linux博客
  
SYSTEM V共享内存 
内核中维护了如下数据结构: 
struct shmid_ds 
 { 
 struct ipc_perm shm_perm; /* 权限信息 */ 
 size_t shm_segsz; /* 共享内存大小 */ 
 pid_t shm_lpid; /* attach或detach的进程id */ 
 pid_t shm_cpid; /* 创建者进程id */ 
 shmatt_t shm_nattch; /* 当前attach的数量 */ 
 time_t shm_atime; /* last-attach time */ 
 time_t shm_dtime; /* last-detach time */ 
 time_t shm_ctime; /* last-change time */ 
 . . . 
}; 
  
基本操作函数: 
创建或获得共享内存 
int shmget ( key_t key, size_t size, int oflag); 
size:创建者指定共享内存的大小,而打开现有共享内存时设置为0 
oflag: IPC_CREAT、IPC_EXCL 
把共享内存attach到进程空间 
void *shmat (int shmid, const void *shmaddr, int flag) 
把共享内存从进程空间detach 
int shmdt (const void*shmaddr); 
读取状态或删除共享内存 
int shmctl(int shmid, int cmd, struct shmid_ds *buff); 
查看系统中的共享内存ipcs –m 
共享内存具有内核持续性。 
Limits 略 
  
第六部分 I/O模型 
  
Linu基本IO模型 
系统共提供四种IO,见下图: 
Linux IPC 和 IO模型 - shenze60 - linux博客
 
  
同步阻塞IO模型 
说明:用户程序执行一个系统调用,使得应用程序阻塞,直到系统调用完成或发生错误。
Linux IPC 和 IO模型 - shenze60 - linux博客
 

   
同步非阻塞IO模型 
说明:用户通过设置O_NONBLOCK标志来实现非阻塞,如果IO操作不能立即满足则会返回一个错误码(通常为EAGAIN或EWOULDBLOCK) 

Linux IPC 和 IO模型 - shenze60 - linux博客

 

  
异步阻塞IO模型 
说明:通过阻塞select系统调用来阻塞实际的系统调用直到完成。 select可以同时处理多个系统调用 

Linux IPC 和 IO模型 - shenze60 - linux博客

   
异步非阻塞IO模型(AIO) 
说明:当请求(read)发出后,系统调用立即返回,进程可以做其它的处理,当请求在内核中完成后,就会产生一个信号或启动一个基于线程的回调函数来进行IO处理。

Linux IPC 和 IO模型 - shenze60 - linux博客