进程间通信IPC
索引:
1.fork、exec和exit对IPC对象的影响
2.fcntl记录锁
3.管道和FIFO的属性
4.管道和FIFO的限制
5.pipe函数
6.popen和pclose
7.mkfifo函数
8.System V IPC共性描述
9.msqid_ds结构
10.msgget函数
11.msgsnd函数
12.msgrcv函数
13.msgctl函数
14.在System V消息队列上使用select和poll
15.semid_ds结构
16.semget函数
17.semop函数
18.semctl函数
19.shmid_ds结构
20.shmget函数
21.shmat函数
22.shmdt函数
23.shmctl函数
24.mmap函数
25.munmap函数
26.msync函数
27.匿名内存映射
1.fork、exec和exit对IPC对象的影响
IPC类型 | fork | exec | _exit |
管道和FIFO | 子进程取得父进程的所有打开着的描述字的拷贝 | 所有打开的描述字继续打开着,除非已设置描述字的FD_CLOEXEC位 | 关闭所有打开着的描述字,最后一个关闭时删除管道或FIFO中残留的所有数据 |
Posix消息队列 | 子进程取得父进程的所有打开着的消息队列描述字的拷贝 | 关闭所有打开着的消息队列描述字 | 关闭所有打开着的消息队列描述字 |
System V消息队列 | 没有效果 | 没有效果 | 没有效果 |
Posix互斥锁、条件变量、读写锁、基于内存的信号灯 | 若驻留在共享内存中而且具有进程间共享属性,则共享 | 除非在继续打开着的共享内存中而且具有进程间共享属性,否则消失 | 除非在继续打开着的共享内存中而且具有进程间共享属性,否则消失 |
Posix有名信号灯 | 父进程中所有打开着的有名信号灯在子进程中继续打开着 | 关闭所有打开着的有名信号灯 | 关闭所有打开着的有名信号灯 |
System V信号灯 | 子进程中所有semadj值都置为0 | 所有semadj值都携入新程序中 | 所有semadj值都加到相应的信号灯上 |
fcntl记录上锁 | 子进程不继承父进程持有的锁 | 只要描述字继续打开着,锁就不变 | 解开由进程持有的所有未处理的锁 |
mmap内存映射和Posix共享内存区 | 父进程中的内存映射存留到子进程中 | 去除内存映射 | 去除内存映射 |
System V共享内存区 | 附接着的共享内存区在子进程中继续附接着 | 断开所有附接着的共享内存区 | 断开所有附接着的共享内存区 |
门 | 子进程取得父进程的所有打开着的描述字,但是客户在门描述字上激活其过程时,只有父进程是服务器 | 所有门描述字都应关闭,因为它们创建时设置了FD_CLOEXEC位 | 关闭所有打开着的描述字 |
2.fcntl记录锁
Unix内核没有文件内记录的概念,这里的记录是指字节范围(byte range)。
Posix记录上锁定义了一个特殊的字节范围以指定整个文件,它的其始偏移为0(文件的开头),长度为0。文件上锁是记录上锁的一个特例。
粒度(granularity)用于标记能被锁住的对象的大小。对于Posix记录上锁来说,粒度就是单个字节。
记录上锁的Posix接口是fcntl函数:
#include <fcntl.h>
int fcntl( int fd, int cmd, …/* struct flock *arg */ );
返回:成功时取决于cmd,出错时为-1。
对应记录上锁的第三个参数arg是指向某个flock结构的指针:
struct flock {
short l_type; /* F_RDLCK, F_WRLCK, F_UNLCK */
short l_whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* relative starting offset in bytes */
off_t l_len; /* #bytes; 0 means until end-of-file */
pid_t pid; /* PID returned by F_GETLK */
};
cmd命令有三个:
- F_SETLK:获取(l_type为F_RDLCK或F_WRLCK)或释放(l_type为F_UNLCK)由arg指向的flock结构所描述的锁。如果该锁无法授予调用进程,该函数就立即返回一个EACCES或EAGAIN错误而不阻塞。
- F_SETLKW:该命令与上一命令相似,不同在于,若所请求的锁无法授予,则调用进程将阻塞到该锁能够授予为止。(W的意思是“等待”)
- F_GETLK:检查由arg指向的锁以确定是否有某个已存在的锁会妨碍新锁授予调用进程。如果当前没有这样的锁存在,由arg指向的flock结构的l_type被置为F_UNLCK。否则,关于这个已存在锁的信息将在由arg指向的flock结构中返回(该结构的内容由fcntl函数覆写),其中包含持有该锁的进程ID。
l_whence成员有三个值:
- SEEK_SET:l_start相对于文件的开头解释;
- SEEK_CUR:l_start相对与文件的当前字节偏移解释;
- SEEK_END:l_start相对于文件的末尾解释。
l_len成员指定从该偏移开始的连续字节数。长度为0表示锁住整个文件,一般锁整个文件如下使用:指定l_whence成员为SEEK_SET,l_start为0,l_len为0。
fcntl记录上锁既可用于读也可用于写,对于一个文件的任意字节,最多只能存在一种类型的锁(读出锁或写入锁)。而且,一个给定字节可以有多个读出锁,但只能有一个写入锁。
当一个描述字不是打开来用于读时,如果我们对它请求一个读出锁,错误就会发生;同样,当一个描述字不是打开来用于写时,请求一个写锁错误也会发生。
对于一个打开着某个文件的给定进程来说,当它关闭该文件的任何一个描述字或终止时,与该文件关联的所有锁都被删除。锁不能通过fork由子进程继承。删除锁的关键是进程ID,而不是引用同一文件的描述字数目及打开目的。
记录上锁不应该同标准I/O函数库一块使用,因为该函数库会执行内部缓冲。当某个文件需要上锁时,为避免问题,应对它使用read和write。
使用fcntl上锁和解锁的例子见unpv22e:lock/lockfcntl.c。
劝告性锁和强制性锁
Posix记录上锁是劝告性锁(advisory locking)。劝告性锁对协作进程(cooperating processes)是足够了。
有些系统提供了强制性锁(mandatory locking)。使用强制性锁后,内核将检查每个read和write请求,以验证其操作不会干扰由某个进程持有的某个锁。对于通常的阻塞式描述字,与某个强制性锁冲突的read或write将把调用进程投入睡眠,直到该锁释放为止。对于非阻塞式描述字,与某个强制性锁冲突的read或write将导致它们返回一个EAGAIN错误。
对某个特定文件施行强制性锁,应满足:
- 组成员执行位必须关闭;
- SGID位必须打开。
强制性锁不需要新的系统调用。
虽然强制性上锁有一定作用,但多个进程在更新同一个文件时,仍然会导致混乱。进程之间还是需要某种上锁形式的协作。
当一个文件区被锁住时,待处理的读出者和写入者的优先级是不可知的。
3.管道和FIFO的属性
非阻塞方式对管道和FIFO的影响(设置方式:open时指定O_NONBLOCK;或使用fcntl使能O_NONBLOCK标志):
当前操作 | 管道或FIFO的现有打开操作 | 阻塞(缺省)时返回 | O_NONBLOCK时返回 |
open FIFO只读 | FIFO打开来写 | 成功返回 | 成功返回 |
FIFO不是打开来写 | 阻塞到FIFO打开来写为止 | 成功返回 | |
open FIFO只写 | FIFO打开来读 | 成功返回 | 成功返回 |
FIFO不是打开来读 | 阻塞到FIFO打开来读为止 | 返回ENXIO错误 | |
从空管道或空FIFO read | 管道或FIFO打开来写 | 阻塞到管道或FIFO中有数据或管道或FIFO不再为写打开为止 | 返回EAGAIN错误 |
管道或FIFO不是打开来写 | read返回0(文件结束符) | read返回0(文件结束符) | |
往管道或FIFO write | 管道或FIFO打开来读 | (见如下说明) | (见如下说明) |
管道或FIFO不是打开来读 | 给线程产生SIGPIPE | 给线程产生SIGPIPE |
其他规则:
如果请求读出的数据量多于管道或FIFO中当前可用数据量,那么只返回这些可用的数据。
如果请求写入的数据的字节数小于或等于PIPE_BUF(一个Posix限制值),那么write操作保证是原子的。
O_NONBLOCK标志的设置对于write操作的原子性没有影响。然而当一个管道或FIFO设置成非阻塞时,来自write的返回值取决于待写的字节数以及该管道或FIFO中当前可用空间的大小。如果待写的字节数小于等于PIPE_BUF:(1)如果该管道或FIFO中有足以存放所请求字节数的空间,那么所有数据字节都写入;(2)如果该管道或FIFO中没有足以存放所请求字节数的空间,那么立即返回一个EAGAIN错误。如果待写的字节数大于PIPE_BUF:(1)如果该管道或FIFO中至少有1字节空间,那么内核写入该管道或FIFO能容纳数目的数据字节,该数目同时作为来自write的返回值;(2)如果该管道或FIFO已满,那么立即返回一个EAGAIN错误。
如果写入一个没有打开着用于读的管道或FIFO,那么内核将产生一个SIGPIPE信号。该信号的缺省动作是终止进程。如果调用进程忽略了该信号,或捕获了该信号并从其信号处理程序中返回,那么write返回一个EPIPE错误。
处理SIGPIPE信号的最容易方法是忽略它,让write返回EPIPE错误,应用应该检查write的返回值。
注意:使用管道的程序,一定要为SIGPIPE信号做好准备。
4.管道和FIFO的限制
系统加于管道和FIFO的唯一限制是:
- OPEN_MAX:一个进程在任意时刻打开的最大描述字数。
- PIPE_BUF:可原子的写往一个管道或FIFO的最大数据量。
OPEN_MAX的值可通过sysconf函数查询。PIPE_BUF的值通常定义在<limits.h>中,但也可在运行时通过调用pathconf或fpathconf取得。
尽管针对管道的PIPE_BUF能够修改,但具体依赖于路径名所存放的底层文件系统,实际应该很少这么做。
5.pipe函数
#include <unistd.h>
int pipe( int fd[2] );
返回:成功时为0,出错时为-1。
创建一个管道,函数返回两个描述字:fd[0]和fd[1],前者打开来读,后者打开来写。
宏S_ISFIFO可用于确定一个描述字或文件是否或是管道,或是FIFO。它的唯一参数是stat结构的st_mode成员,计算结果或为真(非零),或者为假(0)。
管道是通过内核运作的,使用管道传输的每个字节的数据都穿越了用户-内核接口两次:一次是在写入管道时,一次是在从管道读出时。
注意:对管道的read只要该管道中存在一些数据就会马上返回;它不必等待达到所请求的字节数。
6.popen和pclose
#include <stdio.h>
FILE *popen( const char *command, const char *type );
返回:成功时为文件指针,出错时为NULL。
int pclose( FILE *stream );
返回:成功时为shell的终止状态,出错时为-1。
popen函数创建一个管道并启动另一个进程,该进程或者从该管道读出标准输入,或者往该管道写入标准输出。
其中command是一个shell命令行,它由sh程序处理。popen在调用进程和所指定的命令之间创建一个管道,由popen返回的值是一个标准I/O FILE指针,该指针或者用于输入,或者用于输出,具体取决于字符串type:
- 如果type为r,那么调用进程读进command的标准输出。
- 如果type为w,那么调用进程写到command的标准输入。
pclose函数关闭由popen创建的标准I/O流stream,等待其中的命令终止,然后返回shell的终止状态。
7.mkfifo函数
#include <sys/types.h>>
#include <sys/stat.h>
int mkfifo( const char *pathname, mode_t mode );
返回:成功是为0,出错时为-1。
FIFO类似于管道,它是一个单向(半双工)数据流,每个FIFO有一个路径名与之关联,从而允许无亲缘关系的进程访问同一个FIFO,也称为有名管道(named pipe)。
FIFO由mkfifo创建。其中pahtname是一个普通的UNIX路径名,它是该FIFO的名字,mode参数指定文件权限位,类似于open的第三个参数。
mkfifo已经隐含指定O_CREAT|O_EXCL,即要么创建一个新的FIFO,要么返回一个EEXIST错误。
一个FIFO创建完毕后,它必须或者打开来读,或者打开来写,它不能打开来既读又写,因为它是半双工的。
对管道或FIFO的write总是往末尾添加数据,对它们的read总是从开头返回数据。如果对管道或FIFO调用lseek,将返回ESPIPE错误。
打开FIFO进行处理有时序上的问题。如果当前没有任何进程打开某个FIFO来写,那么打开该FIFO来读的进程将阻塞。所以在多进程操作FIFO时要防止死琐的产生。
8.System V IPC共性描述
System V IPC指以下三种类型的IPC:
- System V消息队列
- System V信号灯
- System V共享内存区
所有System V IPC函数列表:
消息队列 | 信号灯 | 共享内存区 | |
头文件 | sys/msg.h | sys/sem.h | sys/shm.h |
创建或打开函数 | msgget | semget | shmget |
控制操作函数 | msgctl | semctl | shmctl |
操作函数 | msgsnd msgrcv | semop | shmat shmdt |
key_t键和ftok函数
System V IPC使用key_t值作为它们的名字。头文件<sys/types.h>把key_t定义为一个整数,它通常是一个至少32位的整数。这些整数通常是由ftok函数赋予的。
ftok函数把一个已存在的路径名和一个整数标识符转换成一个key_t值,称为IPC键(IPC key):
#include <sys/ipc.h>
key_t ftok( const char *pahtname, int id );
返回:成功时为IPC键,出错时为-1。
如果pathname不存在,或者对调用进程不可访问,ftok返回-1。
注意:
- 不能保证两个不同的路径名与同一个id值的组合产生不同的键。
- 用于产生键的pahtname不能是服务器存活期间由它反复创建并删除的文件,否则会导致ftok多次调用返回不同的值。
ipc_perm结构
内核为每个IPC对象维护一个信息结构:
struct ipc_perm {
uid_t uid; /* owner's user id */
gid_t gid; /* owner's group id */
uid_t cuid; /* creator's user id */
gid_t cgid; /* creator's group id */
mode_t mode; /* access modes */
ulong_t seq; /* slot usage sequence number */
key_t key; /* key */
};
创建与打开IPC对象
创建或打开一个IPC对象需要一个类型为key_t的IPC键,对此键,应用有两种选择:
- 调用ftok,给它传递pathname和id;
- 指定IPC_PRIVATE,它保证创建一个新的、唯一的IPC对象。
创建或打开一个IPC对象函数共同的另一个参数是oflag,它指定IPC对象的读写权限位(ipc_perm结构中的mode成员),并选择是创建一个新的IPC对象还是访问一个存在的IPC对象。
选择的规则如下:
oflag标志 | 不存在 | 已存在 |
无特殊标志 | 出错,errno=ENOENT | 成功,引用已存在对象 |
IPC_CREAT | 成功,创建新对象 | 成功,引用已存在对象 |
IPC_CREAT|IPC_EXCL | 成功,创建新对象 | 出错,errno=EEXIST |
注意:设置IPC_EXCL但不设置IPC_CREAT没有意义。
权限位的设置如下(八进制):
- 0400:由用户(属主)读;
- 0200:由用户(属主)写;
- 0040:由(属)组成员读;
- 0020:由(属)组成员写;
- 0004:由其他用户读;
- 0002:由其他用户写;
oflag由选择参数和权限参数组合而成。
ipc_perm结构的cuid和cgid成员分别设置为调用进程的有效用户ID和有效组ID,这两个成员合称为创建者ID。
ipc_perm结构的uid和gid成员也分别设置为调用进程的有效用户ID和有效组ID,这两个成员合称为属主ID。
ipc_perm结构中的seq成员是一个槽位使用情况序列号。该变量是一个由内核为在系统中的每个潜在的IPC对象维护的计数器。每当删除一个IPC对象时,内核就递增相应的槽位号,若溢出则循环回0。这避免在短时间内重用IPC标识符。
9.msqid_ds结构
对于系统中的每个System V消息队列,内核维护一个如下的结构:
struct msqid_ds {
struct ipc_perm msg_perm; /* operation permission struct */
struct msg *msg_first; /* ptr to first message on q */
struct msg *msg_last; /* ptr to last message on q */
unsigned short msg_cbytes; /* current # bytes on q */
msgqnum_t msg_qnum; /* # of messages on q */
msglen_t msg_qbytes; /* max # of bytes on q */
pid_t msg_lspid; /* pid of last msgsnd */
pid_t msg_lrpid; /* pid of last msgrcv */
time_t msg_stime; /* last msgsnd time */
time_t msg_rtime; /* last msgrcv time */
time_t msg_ctime; /* last change time */
};
10.msgget函数
#include <sys/msg.h>
int msgget( key_t key, int oflag );
返回:成功时为非负标识符,出错时为-1。
用于创建一个新的System V消息队列或访问一个已经存在的消息队列。
参数key和oflag的说明见前。返回值是一个整数标识符,其他三个msg函数用它来指代该队列。
当创建一个消息队列时,msqid_ds结构的如下成员被初始化:
- msg_perm结构的uid和cuid被设置为当前进程的有效用户ID,gid和cgid被设置为当前用户的有效组ID;
- oflag中的读写权限位存放在msg_perm.mode中;
- msg_qnum、msg_lspid、msg_lrpid、msg_stime和msg_rtime被置为0;
- msg_ctime被设置成当前时间;
- msg_qbytes被设置为系统限制值。
11.msgsnd函数
#include <sys/msg.h>
int msgsnd(int msgid, const void *ptr, size_t length, int flag);
返回:成功时为0,出错时为-1。
该函数用于往消息队列上放置一个消息。
msgid是msgget返回的标识符,ptr是一个结构指针,该结构有如下的模板:
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[ 1 ]; /* message data */
};
消息类型mtype必须大于0,因为非正消息类型有特殊的指示作用。
length参数以字节为单位指定待发送消息的长度。这是位于长整数消息类型之后的用户自定义数据的长度,该长度可以是0。
flag参数可以是0,也可以是IPC_NOWAIT。IPC_NOWAIT标志使得msgsnd调用非阻塞。当有如下情形之一时:
- 在指定的队列中已经有太多的字节(对应msqid_ds结构中的msg_qbytes值);
- 在系统范围存在太多的消息。
若设置了IPC_NOWAIT,则msgsnd立即返回,返回一个EAGAIN错误。若未指定该标志,则msgsnd阻塞,直到:
- 具备存放新消息的空间;
- 有msgid标识的消息队列被删除,此时返回EIDRM错误;
- 被信号中断,此时返回 EINTR错误。
12.msgrcv函数
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *ptr, size_t length, long type, int flag );
返回:成功时为读入缓冲区中数据的字节数,出错时为-1。
该函数从某个消息队列中读出一个消息。
ptr参数指定所接收消息的存放位置。跟msgsnd一样,该指针指向紧挨在真正的消息数据之前返回的长整数类型字段。
length指定由ptr指向的缓冲区中数据部分的大小。这是该函数能返回的最大数据量。该长度不包含长整数类型字段。
type指定希望从所给定的队列中读出什么样的消息:
- type为0,返回队列中第一个消息。每个消息队列是作为一个FIFO链表维护的,所以返回的是队列中最早的消息。
- type大于0,返回其类型值为type的第一个消息。
- type小于0,返回其类型值小于或等于type参数的绝对值的消息中类型值最小的第一个消息。
flag参数指定所请求的消息不在队列中时怎么办。在没有消息时,若设置了IPC_NOWAIT标志,则函数立即返回一个ENOMSG错误;否则,调用者阻塞知道如下某个时间发生:
- 有一个所请求类型的消息可获取;
- 由msqid标识的消息队列被删除,此时返回个EIDRM错误;
- 被某个捕获的信号中断,此时返回 EINTR错误。
13.msgctl函数
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf );
返回:成功时为0,出错时为-1。
该函数提供在一个消息队列上的各种控制操作。
msgctl提供三个命令:
- IPC_RMID:从系统中删除由msqid指定的消息队列。当前在该队列上的任何消息都被丢弃。此时。第三个参数忽略不用。
- IPC_SET:给指定的消息队列设置其msqid_ds结构的以下四个成员:msg_perm.uid、msg_perm.gid、msg_perm.mode和msg_perm.qbytes。它们的值来自buff指向的结构中的相应成员。
- IPC_STAT:通过buff参数给调用者返回所指定消息队列中的当前msqid_ds结构。
14.在System V消息队列上使用select和poll
System V消息队列通过标识符而不是描述字标识,所以不能在消息队列上直接使用select和poll。
解决问题的方法之一是:让服务器创建一个管道,然后派生一个子进程,由子进程阻塞在msgrcv调用中。当有消息准备好被处理时,msgrcv返回,子进程读出该消息,并把消息写入管道。服务器父进程可能在管道以及一些网络连接上select。这种办法的负面效果是消息被处理了三次,为避免此种情况,父进程可以创建一个在它自身和子进程之间分享的共享内存区,然后把管道用作父子进程见的一种标志。
与网络编程相比,System V消息队列的另一个遗失特性是无法窥探一个消息,而这是recv、recvfrom和recvmsg函数的MSG_PEEK标志提供的能力。
15.semid_ds结构
System V信号灯是信号灯集的概念:一个或多个信号灯构成一个集合。对于系统每个信号灯集,内核维护如下的一个结构:
struct semid_ds {
struct ipc_perm sem_perm; /* operation permission struct */
struct sem *sem_base; /* ptr to first semaphore in set */
unsigned short sem_nsems; /* # of semaphores in set */
time_t sem_otime; /* last semop time */
time_t sem_ctime; /* last change time */
};
当前信号灯集中的每个信号灯对应一个sem结构。定义如下:
struct sem {
signed short semval; /* semaphore text map address */
pid_t sempid; /* pid of last operation */
unsigned short semncnt; /* # awaiting semval > cval */
unsigned short semzcnt; /* # awaiting semval = 0 */
};
16.semget函数
#include <sys/sem.h>
int semget(key_t key, int nsems, int oflag);
返回:成功时为非负标识符,出错时为-1。
创建一个信号灯集或访问一个已存在的信号灯集。
返回值是信号灯标识符,供其他信号灯函数使用。
nsems是集合中的信号灯数。如果不是创建一个信号灯集,而只是访问已存在的集合,则该参数可以指定为0。一旦创建完毕一个信号灯集,就不能改变其中的信号灯数。
当实际操作为创建一个新的信号灯集时,semid_ds结构的以下成员将被初始化:
- sem_perm结构的uid和cuid被设置为调用进程的有效用户ID,gid和cgid被设置为调用进程的有效组ID;
- oflag参数中的读写权限存入sem_perm.mode中;
- sem_otime被设置为0,sem_ctime被置为当前时间;
- sem_nsems被置为nsems参数的值;
- 与该集合中每个信号灯关联的各个sem结构并不初始化。这些结构必须是在以SETVAL或SETALL命令调用semctl时初始化的。
System V信号灯的创建和初始化需两次函数调用是一个致命的缺陷,这会导致竞争状态的出现。
解决竞争状态的方法是:当semget创建一个新的信号灯集时,其semid_ds结构的sem_otime成员保证被设置为0。该成员只是在semop调用成功时才被设置为当前值。在调用semget进行访问而不是创建时,以IPC_STAT命令调用semctl,然后等待sem_otime变为非零值。到时就可断定该信号灯已经被初始化,而且对它初始化的进程已成功完成semop调用。所以,创建该信号灯集的进程必须初始化它的值,而且必须在任何其他进程可以使用该信号灯集之前调用semop。
例子程序见:unpv22e:lock/locksvsem.c。
17.semop函数
#include <sys/sem.h>
int semop( int semid, struct sembuf *opsptr, size_t nops);
返回:成功时为0,出错时为-1。
对一个或多个信号灯进行操作。
opsptr指向如下结构模板的数组(该结构可能不止如下几个成员):
struct sembuf {
shrot sem_num; /* semaphore number:0,1,..,nsems-1 */
short sem_op; /* semaphore operation: < 0,0, >0 */
short sem_flg; /* operation flags:0,IPC_NOWAIT,SEM_UNDO */
};
nops参数指出结构数组中元素的个数。每个元素给目标信号灯集中某个信号灯指定一个操作。特定的信号灯由sem_num指定;sem_op指定特定的操作;sem_flg指定非阻塞(IPC_NOWAIT)、恢复等标志。在阻塞、非阻塞情况下返回的错误情况与其他System V IPC相同。
semop函数由内核保证原子的执行,内核或者完成所有操作,或者什么也不做。
semop操作的具体描述:
- 如果sem_op是正数,其值就加到semval(信号灯的当前值)上,这对应于释放由某个信号灯控制的资源。如果指定了SEM_UNDO标志,就从相应信号灯的semadj值中减掉sem_op的值。
- 如果sem_op是0,那么调用者希望等待到semval变为0,如果semval已经是0,则立即返回;如果semval不为0,相应信号灯的semzcnt(等待semval变为0的线程数)值就加1,调用线程阻塞到semval变为0(那时semzcnt再减1)。若指定了IPC_NOWAIT,则调用线程不会睡眠,返回EAGAIN。
- 如果sem_op是负数,那么调用者希望等待semval变为大于或等于sem_op的绝对值,这对应于分配资源。如果semval大于或等于sem_op的绝对值,则从semval中减掉sem_op的绝对值,如果指定了SEM_UNDO,那么sem_op的绝对值就加到相应信号灯的semadj值上。如果semval小于sem_op的绝对值,相应信号灯的semncnt值就加1,调用线程阻塞直到semval变为大于或等于sem_op的绝对值。若指定了IPC_NOWAIT,则调用线程不会睡眠,返回EAGAIN。
semadj称为指定信号灯针对调用进程的调整值。当调用进程终止时,semadj的值就加到相应信号灯的semval上。若调用进程对某个信号灯的全部操作都指定SEM_UNDO标志,则该进程终止时,该信号灯的值就会变得像根本没有运行过该进程一样,这就是复旧(undo)的本意。
18.semctl函数
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, …/* union arg */);
返回:成功时为非负值,出错时为-1。
对一个信号灯执行各种控制操作。
semnum标识某个信号灯,semnum仅仅用于GETVAL、SETVAL、GETNCNT、GETZCNT和GETPID命令。
第四个参数是可选的,它依赖于第三个参数cmd。它是一个联合:
union semun {
int val; /* used for SETVAL only */
struct semid_ds *buf; /* used fro IPC_SET and IPC_STAT */
ushort *array; /* used for GETALL and SETALL */
};
该联合没有出现在任何系统头文件中,由应用程序声明。而且它是以值传递的,而不是以引用传递的。
System V支持如下cmd值(除特别声明,成功时返回0,失败返回-1):
- GETVAL:把semval的当前值作为函数返回值返回。
- SETVAL:把semval设置为arg.val。如果操作成功,那么相应信号灯在所在进程中的调整值(semadj)将被置为0。
- GETPID:把sempid的当前值作为函数值返回。
- GETNCNT:把semncnt的当前值作为函数值返回。
- GETZCNT:把semzcnt的当前值作为函数值返回。
- GETALL:返回所指定信号灯集的每个成员的semval值。这些值通过arg.array指针返回。函数本身返回值为0。注意,调用者必须分配足够容纳所指定信号灯集中所有成员的semval值的一个unsigned short整数数组,然后把arg.array设置成指向这个数组。
- SETALL:设置所指定信号灯集中每个成员的semval值。这些值通过arg.array数组指定。
- IPC_RMID:把由semid指定的信号灯集从系统中删除。
- IPC_SET:设置semid_ds结构中的以下三个成员:sem_perm.uid、sem_perm.gid和sem_perm.mode。这些值来自由arg.buf参数指向的结构中相应成员。semid_ds中的sem_ctime成员也被设置为当前值。
- IPC_STAT:通过arg.buf参数返回当前的semid_ds结构。注意,调用者必须首先分配一个semid_ds结构,并把arg.buf设置为指向这个结构。
19.shmid_ds结构
对于每个System V共享内存区,内核维护如下的信息结构:
struct shmid_ds {
struct ipc_perm shm_perm; /* operation permission struct */
size_t shm_segsz; /* size of segment in bytes */
pid_t shm_lpid; /* pid of last shmop */
pid_t shm_cpid; /* pid of creator */
shmatt_t shm_nattch; /* current # attached */
shmat_t shm_cnattch; /* in-core # attached */
time_t shm_atime; /* last shmat time */
time_t shm_dtime; /* last shmdt time */
time_t shm_ctime; /* last change time */
};
20.shmget函数
#include <sys/shm.h>
int shmget( key_t key, size_t size, int oflag );
返回:成功时为共享内存区对象,出错时为-1。
函数创建一个尚未存在的共享内存区,或者访问一个已存在的共享内存区。
返回值是共享内存区标识符,供其他函数使用。
size参数以字节为单位指定内存的大小。当实际操作为创建一个新的内存区时,必须指定一个不为0的size值;如果实际操作是访问一个已存在的共享内存区,则size应为0。
当实际操作为创建一个新的内存区时,该内存区被初始化为size个字节的0。
21.shmat函数
#include <sys/shm.h>
void * shmat( int shmid, const void *shmaddr, int flag );
返回:成功时为映射区的其始地址,出错时为-1。
调用shmat将共享内存区附接到调用进程的地址空间。
shmid是shmget的返回值。shmat的返回值是所指定的共享内存区在调用进程内的起始地址。确定此地址的规则如下:
- 如果shmaddr是空指针,则系统替调用者选择地址。这是推荐(也是可移植性最好的)方法。
- 如果shmaddr非空,则返回地址取决于调用者是否给flag参数指定了SHM_RND值。如果SHM_RND没有指定,则共享内存区附接到由shmaddr指定的地址;若指定SHM_RND,则附接到由shmaddr指定的地址向下舍入一个SHMLBA常值。LBA代表“低端边界地址(lower boundary address)。
flag参数可以指定SHM_RDONLY值,它限定只读访问。
22.shmdt函数
#include <sys/shm.h>
int shmdt( const void *shmaddr );
返回:成功时为0,出错时为-1。
调用shmdt断开与共享内存区的连接。
当一个进程终止时,它的所有当前附接着的共享内存区都自动断接掉。
23.shmctl函数
#include <sys/shm.h>
int shmctl( int shmid, int cmd, struct shmid_ds *buff );
返回:成功时为0,出错时为-1。
函数提供三个命令:
- IPC_RMID:从系统中删除由shmid标识的共享内存区并拆除它。
- IPC_SET: 给所指定的共享内存区设置其shmid_ds结构的以下三个成员:shm_perm.uid、shm_perm.gid和shm_perm.mode,它们的值来自参数中的相应成员。shm_ctime的值用当前时间替换。
- IPC_STAT:向调用者返回所指定共享内存区的当前shmid_ds结构。
24.mmap函数
#include <sys/mman.h>
void *mmap( void *addr, size_t len, int prot, int flags, int fd, off_t offset );
返回:成功时为被映射区的起始地址,出错时为MAP_FAILD。
mmap函数把一个文件或一个Posix共享内存区对象映射到调用进程的地址空间。使用该函数有三个目的:
- 使用普通文件以提供内存映射;
- 使用特殊文件以提供匿名内存映射;
- 使用shm_open以提供无亲缘关系进程间的Posix共享内存区。
其中addr可以指定为描述字fd应被映射到的进程内空间的起始地址。它通常被指定为一个空指针,让内核自己去选择起始地址。Len是映射到调用进程地址空间的字节数,它从被映射文件开头起offset个字节处开始算。
内存映射区的保护由prot参数指定,它使用如下的常值。该参数的常见值是PROT_READ|PROT_WRITE:
- PROT_READ:数据可读;
- PROT_WRITE:数据可写;
- PROT_EXEC:数据可执行;
- PROT_NONE:数据不可访问。
flags使用如下的常值。MAP_SHARED或MAP_PRIVATE这两个标志必须指定一个,并可有选择的或上MAP_FIXED。MAP_PRIVATE的含义是调用进程对被映射数据所作的修改只对该进程可见,而不改变其底层支撑对象(或者是一个文件对象,或者是一个共享内存区对象)。MAP_SHARED的含义则是调用进程对被映射数据所作的修改对于共享该对象的所有进程都可见,而且确实改变了其底层支撑对象。从移植性上考虑,MAP_FIXED不应该指定:
- MAP_SHARED:变动是共享;
- MAP_PRIVATE:变动是私自的;
- MAP_FIXED:准确的解释addr参数。
mmap成功返回后,fd参数可以关闭。该操作对于由mmap建立的映射关系没有影响。
父子进程之间共享内存区的方法之一是,父进程在调用fork前指定MAP_SHARED调用mmap。Posix.1保证父进程中的内存映射关系存留到子进程中,而且父进程所作的修改子进程能看到,反过来也一样。
不是所有的文件都能进行内存映射。如把一个访问终端或套接口的描述字映射到内存将导致mmap返回一个错误。
内存映射区的大小(mmap的第二个参数)可以与文件的大小不同。但是,内核跟踪着被内存映射的底层支撑对象的大小,而且我们总是能访问在当前文件大小以内又在内存映射区以内的那些字节。
25.munmap函数
#include <sys/mman.h>
int munmap( void *addr, size_t len );
返回:成功时为0,出错时为-1。
其中addr是mmap返回的地址,len是映射区的大小。
从进程地址空间删除一个映射关系。之后再次访问这些地址将导致向调用进程产生一个SIGSEGV信号。如果映射区用MAP_PRIVATE标志映射,那么调用进程对它所作的变动都被丢弃。
26.msync函数
#include <sys/mman.h>
int msync( void *addr, size_t len, int flags );
返回:成功时为0,出错时为-1。
内核的虚存算法保持内存映射文件(一般在硬盘上)与内存映射区(在内存中)的同步(使用了MAP_SHARED),如果我们修改了内存映射到某个文件的内存区中某个位置的内容,那么内核将在稍后某个时刻相应的更新文件。如果我们希望确信硬盘上的文件内容与内存映射区中的内容一致,则调用msync来执行这种同步。
其中addr和len参数通常指代内存中的整个内存映射区,不过也可指定该内存区的一个子集。flags是如下常值的组合:
- MS_ASYNC:执行异步写;
- MS_SYNC:执行同步写;
- MS_INVALIDATE:是高速缓存的数据失效。
MS_ASYNC和MS_SYNC这两个值必须指定一个,但不能都指定。两者的差别是,一旦写操作已由内核排入队列,MS_ASYNC即返回,而MS_SYNC则要等到写操作完成后返回。如果还指定了MS_INVALIDATE,那么与其最终拷贝不一致的文件数据的所有内存中拷贝都失效,后续的引用将从文件中取得数据。
27.匿名内存映射
如果调用mmap的目的是提供一个即将穿越fork由父子进程共享的映射内存区,则可以不用创建一个文件,在open它等一系列操作。具体依赖于实现:
- 4.4BSD提供匿名内存映射,它彻底避免了文件的创建和打开。方法是把mmap的flags参数指定为MAP_SHARED|MAP_ANON,把fd参数指定为-1,offset参数被忽略。这样的内存区初始化为0。
- SVR4提供/dev/zero设备文件,我们open它之后可在mmap调用中使用得到的描述字。从该设备读时返回的字节全为0,写往该设备的任何字节则被丢弃。(许多源自Berkeley的实现也支持/dev/zero,如SunOs4.1.x和BSD/OS3.1)。
例子见unpv22e:shm/incr_map_anon.c和shm/incr_dev_zero.c。