主要内容:
第一部分 信号
第二部分 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)移植性:在某些系统中实现了双工管道,但是为了移植性不建议使用
有名管道(FIFO)
int mkfifo (const char * pathname, mode_t mode)
第一个参数是一个普通的路径名,即创建后FIFO的名字。路径名只能位于本地文件系统中,并且如果路径名是一个已经存在的路径名时,会返回EEXIST错误。
第二个参数是打开模式。
打开规则(open):
(1)读打开:阻塞直到有进程为写而打开
(2)写打开:阻塞直到有进程为读而打开
读写规则类似于pipe()
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,这样就没有了数据项
基本操作函数:
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消息队列
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信号量(内核中实现)
有名信号量:
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的数量
}
基本操作函数:
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字节。
其它说明:
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);
共享内存和文件映射的异同:
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,见下图:
同步阻塞IO模型
说明:用户程序执行一个系统调用,使得应用程序阻塞,直到系统调用完成或发生错误。
同步非阻塞IO模型
说明:用户通过设置O_NONBLOCK标志来实现非阻塞,如果IO操作不能立即满足则会返回一个错误码(通常为EAGAIN或EWOULDBLOCK)
异步阻塞IO模型
说明:通过阻塞select系统调用来阻塞实际的系统调用直到完成。 select可以同时处理多个系统调用
异步非阻塞IO模型(AIO)
说明:当请求(read)发出后,系统调用立即返回,进程可以做其它的处理,当请求在内核中完成后,就会产生一个信号或启动一个基于线程的回调函数来进行IO处理。