分类: LINUX
2007-12-21 18:44:03
IPC类型 | fork | exec | _exit |
---|---|---|---|
管道和FIFO | 子进程取得父进程的所有打开着的描述字的拷贝 | 所有打开的描述字继续打开着,除非已设置描述字的FD_CLOEXEC位 | 关闭所有打开着的描述字,最后一个关闭时删除管道或FIFO中残留的所有数据 |
Posix消息队列 | 子进程取得父进程的所有打开着的消息队列描述字的拷贝 | 关闭所有打开着的消息队列描述字 | 关闭所有打开着的消息队列描述字 |
SystemV消息队列 | 没有效果 | 没有效果 | 没有效果 |
Posix互斥锁、条件变量、读写锁、基于内存的信号灯 | 若驻留在共享内存中而且具有进程间共享属性,则共享 | 除非在继续打开着的共享内存中而且具有进程间共享属性,否则消失 | 除非在继续打开着的共享内存中而且具有进程间共享属性,否则消失 |
Posix有名信号灯 | 父进程中所有打开着的有名信号灯在子进程中继续打开着 | 关闭所有打开着的有名信号灯 | 关闭所有打开着的有名信号灯 |
SystemV信号灯 | 子进程中所有semadj值都置为0 | 所有semadj值都携入新程序中 | 所有semadj值都加到相应的信号灯上 |
fcntl记录上锁 | 子进程不继承父进程持有的锁 | 只要描述字继续打开着,锁就不变 | 解开由进程持有的所有未处理的锁 |
mmap内存映射和Posix共享内存区 | 父进程中的内存映射存留到子进程中 | 去除内存映射 | 去除内存映射 |
SystemV共享内存区 | 附接着的共享内存区在子进程中继续附接着 | 断开所有附接着的共享内存区 | 断开所有附接着的共享内存区 |
门 | 子进程取得父进程的所有打开着的描述字,但是客户在门描述字上激活其过程时,只有父进程是服务器 | 所有门描述字都应关闭,因为它们创建时设置了FD_CLOEXEC位 | 关闭所有打开着的描述字 |
Unix内核没有文件内记录的概念,这里的记录是指字节范围(byte range)。
Posix记录上锁定义了一个特殊的字节范围以指定整个文件,它的其始偏移为0(文件的开头),长度为0。文件上锁是记录上锁的一个特例。
粒度(granularity)用于标记能被锁住的对象的大小。对于Posix记录上锁来说,粒度就是单个字节。
记录上锁的Posix接口是fcntl函数:
#include
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命令有三个:
l_whence成员有三个值:
l_len成员指定从该偏移开始的连续字节数。长度为0表示锁住整个文件,一般锁整个文件如下使用:指定l_whence成员为SEEK_SET,l_start为0,l_len为0。
fcntl记录上锁既可用于读也可用于写,对于一个文件的任意字节,最多只能存在一种类型的锁(读出锁或写入锁)。而且,一个给定字节可以有多个读出锁,但只能有一个写入锁。
当一个描述字不是打开来用于读时,如果我们对它请求一个读出锁,错误就会发生;同样,当一个描述字不是打开来用于写时,请求一个写锁错误也会发生。
对于一个打开着某个文件的给定进程来说,当它关闭该文件的任何一个描述字或终止时,与该文件关联的所有锁都被删除。锁不能通过fork由子进程继承。删除锁的关键是进程ID,而不是引用同一文件的描述字数目及打开目的。
记录上锁不应该同标准I/O函数库一块使用,因为该函数库会执行内部缓冲。当某个文件需要上锁时,为避免问题,应对它使用read和write。
Posix记录上锁是劝告性锁(advisory locking)。劝告性锁对协作进程(cooperating processes)是足够了。
有些系统提供了强制性锁(mandatory locking)。使用强制性锁后,内核将检查每个read和write请求,以验证其操作不会干扰由某个进程持有的某个锁。对于通常的阻塞式描述字,与 某个强制性锁冲突的read或write将把调用进程投入睡眠,直到该锁释放为止。对于非阻塞式描述字,与某个强制性锁冲突的read或write将导致 它们返回一个EAGAIN错误。
对某个特定文件施行强制性锁,应满足:
强制性锁不需要新的系统调用。
虽然强制性上锁有一定作用,但多个进程在更新同一个文件时,仍然会导致混乱。进程之间还是需要某种上锁形式的协作。
当一个文件区被锁住时,待处理的读出者和写入者的优先级是不可知的。
非阻塞方式对管道和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:
如果待写的字节数大于PIPE_BUF:
如果写入一个没有打开着用于读的管道或FIFO,那么内核将产生一个SIGPIPE信号。该信号的缺省动作是终止进程。如果调用进程忽略了该信号,或捕获了该信号并从其信号处理程序中返回,那么write返回一个EPIPE错误。
处理SIGPIPE信号的最容易方法是忽略它,让write返回EPIPE错误,应用应该检查write的返回值。
注意:使用管道的程序,一定要为SIGPIPE信号做好准备。
系统加于管道和FIFO的唯一限制是:
OPEN_MAX的值可通过sysconf函数查询。PIPE_BUF的值通常定义在
尽管针对管道的PIPE_BUF能够修改,但具体依赖于路径名所存放的底层文件系统,实际应该很少这么做。
#include
int pipe(int fd[2]);
返回:成功时为0,出错时为-1
创建一个管道,函数返回两个描述字:fd[0]和fd[1],前者打开来读,后者打开来写。
宏S_ISFIFO可用于确定一个描述字或文件是否或是管道,或是FIFO。它的唯一参数是stat结构的st_mode成员,计算结果或为真(非零),或者为假(0)。
管道是通过内核运作的,使用管道传输的每个字节的数据都穿越了用户-内核接口两次:一次是在写入管道时,一次是在从管道读出时。
注意:对管道的read只要该管道中存在一些数据就会马上返回;它不必等待达到所请求的字节数。
#include
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:
pclose函数关闭由popen创建的标准I/O流stream,等待其中的命令终止,然后返回shell的终止状态。
#include
#include
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时要防止死琐的产生。
SystemV IPC指以下三种类型的IPC:
所有SystemV IPC函数列表:
消息队列 | 信号灯 | 共享内存区 | |
---|---|---|---|
头文件 | sys/msg.h | sys/sem.h | sys/shm.h |
创建或打开函数 | msgget | semget | shmget |
控制操作函数 | msgctl | semctl | shmctl |
操作函数 | msgsnd msgrcv |
semop | shmat shmdt |
SystemV IPC使用key_t值作为它们的名字。头文件
ftok函数把一个已存在的路径名和一个整数标识符转换成一个key_t值,称为IPC键(IPC key):
#include
key_t ftok(const char *pahtname, int id);
返回:成功时为IPC键,出错时为-1
如果pathname不存在,或者对调用进程不可访问,ftok返回-1。
注意:
内核为每个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对象需要一个类型为key_t的IPC键,对此键,应用有两种选择:
创建或打开一个IPC对象函数共同的另一个参数是oflag,它指定IPC对象的读写权限位(ipc_perm结构中的mode成员),并选择是创建一个新的IPC对象还是访问一个存在的IPC对象。
选择的规则如下:
oflag标志 | 不存在 | 已存在 |
---|---|---|
无特殊标志 | 出错,errno=ENOENT | 成功,引用已存在对象 |
IPC_CREAT | 成功,创建新对象 | 成功,引用已存在对象 |
IPC_CREAT|IPC_EXCL | 成功,创建新对象 | 出错,errno=EEXIST |
注意:设置IPC_EXCL但不设置IPC_CREAT没有意义。
权限位的设置如下(八进制):
oflag由选择参数和权限参数组合而成。
ipc_perm结构的cuid和cgid成员分别设置为调用进程的有效用户ID和有效组ID,这两个成员合称为创建者ID。
ipc_perm结构的uid和gid成员也分别设置为调用进程的有效用户ID和有效组ID,这两个成员合称为属主ID。
ipc_perm结构中的seq成员是一个槽位使用情况序列号。该变量是一个由内核为在系统中的每个潜在的IPC对象维护的计数器。每当删除一个IPC对象时,内核就递增相应的槽位号,若溢出则循环回0。这避免在短时间内重用IPC标识符。
对于系统中的每个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 */
};
#include
int msgget(key_t key, int oflag);
返回:成功时为非负标识符,出错时为-1
用于创建一个新的SystemV消息队列或访问一个已经存在的消息队列。
参数key和oflag的说明见前。返回值是一个整数标识符,其他三个msg函数用它来指代该队列。
当创建一个消息队列时,msqid_ds结构的如下成员被初始化:
#include
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调用非阻塞。当有如下情形之一时:
若设置了IPC_NOWAIT,则msgsnd立即返回,返回一个EAGAIN错误。若未指定该标志,则msgsnd阻塞,直到:
#include
ssize_t msgrcv(int msqid, void *ptr, size_t length, long type, int flag);
返回:成功时为读入缓冲区中数据的字节数,出错时为-1
该函数从某个消息队列中读出一个消息。
ptr参数指定所接收消息的存放位置。跟msgsnd一样,该指针指向紧挨在真正的消息数据之前返回的长整数类型字段。
length指定由ptr指向的缓冲区中数据部分的大小。这是该函数能返回的最大数据量。该长度不包含长整数类型字段。
type指定希望从所给定的队列中读出什么样的消息:
flag参数指定所请求的消息不在队列中时怎么办。在没有消息时,若设置了IPC_NOWAIT标志,则函数立即返回一个ENOMSG错误;否则,调用者阻塞直到如下某个时间发生:
#include
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
返回:成功时为0,出错时为-1
该函数提供在一个消息队列上的各种控制操作。
msgctl提供三个命令:
SystemV消息队列通过标识符而不是描述字标识,所以不能在消息队列上直接使用select和poll。
解决问题的方法之一是:让服务器创建一个管道,然后派生一个子进程,由子进程阻塞在msgrcv调用中。当有消息准备好被处理时,msgrcv返 回,子进程读出该消息,并把消息写入管道。服务器父进程可能在管道以及一些网络连接上select。这种办法的负面效果是消息被处理了三次,为避免此种情 况,父进程可以创建一个在它自身和子进程之间分享的共享内存区,然后把管道用作父子进程间的一种标志。
与网络编程相比,SystemV消息队列的另一个遗失特性是无法窥探一个消息,而这是recv、recvfrom和recvmsg函数的MSG_PEEK标志提供的能力。
SystemV信号灯是信号灯集的概念:一个或多个信号灯构成一个集合。对于系统每个信号灯集,内核维护如下的一个结构:
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 */
};
#include
int semget(key_t key, int nsems, int oflag);
返回:成功时为非负标识符,出错时为-1
创建一个信号灯集或访问一个已存在的信号灯集。
返回值是信号灯标识符,供其他信号灯函数使用。
nsems是集合中的信号灯数。如果不是创建一个信号灯集,而只是访问已存在的集合,则该参数可以指定为0。一旦创建完毕一个信号灯集,就不能改变其中的信号灯数。
当实际操作为创建一个新的信号灯集时,semid_ds结构的以下成员将被初始化:
SystemV信号灯的创建和初始化需两次函数调用是一个致命的缺陷,这会导致竞争状态的出现。
解决竞争状态的方法是:当semget创建一个新的信号灯集时,其semid_ds结构的sem_otime成员保证被设置为0。该成员只是在 semop调用成功时才被设置为当前值。在调用semget进行访问而不是创建时,以IPC_STAT命令调用semctl,然后等待sem_otime 变为非零值。到时就可断定该信号灯已经被初始化,而且对它初始化的进程已成功完成semop调用。所以,创建该信号灯集的进程必须初始化它的值,而且必须 在任何其他进程可以使用该信号灯集之前调用semop。
#include
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)、恢复等标志。在阻塞、非阻塞情况下返回的错误情况与其他SystemV IPC相同。
semop函数由内核保证原子的执行,内核或者完成所有操作,或者什么也不做。
semop操作的具体描述:
semadj称为指定信号灯针对调用进程的调整值。当调用进程终止时,semadj的值就加到相应信号灯的semval上。若调用进程对某个信号灯 的全部操作都指定SEM_UNDO标志,则该进程终止时,该信号灯的值就会变得像根本没有运行过该进程一样,这就是复旧(undo)的本意。
#include
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 */
};
该联合没有出现在任何系统头文件中,由应用程序声明。而且它是以值传递的,而不是以引用传递的。
SystemV支持如下cmd值(除特别声明,成功时返回0,失败返回-1):
对于每个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 */
};
#include
int shmget(key_t key, size_t size, int oflag);
返回:成功时为共享内存区对象,出错时为-1
函数创建一个尚未存在的共享内存区,或者访问一个已存在的共享内存区。
返回值是共享内存区标识符,供其他函数使用。
size参数以字节为单位指定内存的大小。当实际操作为创建一个新的内存区时,必须指定一个不为0的size值;如果实际操作是访问一个已存在的共享内存区,则size应为0。
当实际操作为创建一个新的内存区时,该内存区被初始化为size个字节的0。
#include
void * shmat(int shmid, const void *shmaddr, int flag);
返回:成功时为映射区的其始地址,出错时为-1
调用shmat将共享内存区附接到调用进程的地址空间。
shmid是shmget的返回值。shmat的返回值是所指定的共享内存区在调用进程内的起始地址。确定此地址的规则如下:
flag参数可以指定SHM_RDONLY值,它限定只读访问。
#include
int shmdt(const void *shmaddr);
返回:成功时为0,出错时为-1
调用shmdt断开与共享内存区的连接。
当一个进程终止时,它的所有当前附接着的共享内存区都自动断接掉。
#include
int shmctl(int shmid, int cmd, struct shmid_ds *buff);
返回:成功时为0,出错时为-1
函数提供三个命令:
#include
void *mmap(void *addr, size_t len, int prot, int flags, int fd, \
off_t offset);
返回:成功时为被映射区的起始地址,出错时为MAP_FAILD
mmap函数把一个文件或一个Posix共享内存区对象映射到调用进程的地址空间。使用该函数有三个目的:
其中addr可以指定为描述字fd应被映射到的进程内空间的起始地址。它通常被指定为一个空指针,让内核自己去选择起始地址。len是映射到调用进程地址空间的字节数,它从被映射文件开头起offset个字节处开始算。
内存映射区的保护由prot参数指定,它使用如下的常值。该参数的常见值是PROT_READ|PROT_WRITE:
flags使用如下的常值。MAP_SHARED或MAP_PRIVATE这两个标志必须指定一个,并可有选择的或上MAP_FIXED。 MAP_PRIVATE的含义是调用进程对被映射数据所作的修改只对该进程可见,而不改变其底层支撑对象(或者是一个文件对象,或者是一个共享内存区对 象)。MAP_SHARED的含义则是调用进程对被映射数据所作的修改对于共享该对象的所有进程都可见,而且确实改变了其底层支撑对象。从移植性上考虑, MAP_FIXED不应该指定:
mmap成功返回后,fd参数可以关闭。该操作对于由mmap建立的映射关系没有影响。
父子进程之间共享内存区的方法之一是,父进程在调用fork前指定MAP_SHARED调用mmap。Posix.1保证父进程中的内存映射关系存留到子进程中,而且父进程所作的修改子进程能看到,反过来也一样。
不是所有的文件都能进行内存映射。如把一个访问终端或套接口的描述字映射到内存将导致mmap返回一个错误。
内存映射区的大小(mmap的第二个参数)可以与文件的大小不同。但是,内核跟踪着被内存映射的底层支撑对象的大小,而且我们总是能访问在当前文件大小以内又在内存映射区以内的那些字节。
#include
int munmap(void *addr, size_t len);
返回:成功时为0,出错时为-1
其中addr是mmap返回的地址,len是映射区的大小。
从进程地址空间删除一个映射关系。之后再次访问这些地址将导致向调用进程产生一个SIGSEGV信号。如果映射区用MAP_PRIVATE标志映射,那么调用进程对它所作的变动都被丢弃。
#include
int msync(void *addr, size_t len, int flags);
返回:成功时为0,出错时为-1
内核的虚存算法保持内存映射文件(一般在硬盘上)与内存映射区(在内存中)的同步(使用了MAP_SHARED),如果我们修改了内存映射到某个文 件的内存区中某个位置的内容,那么内核将在稍后某个时刻相应的更新文件。如果我们希望确信硬盘上的文件内容与内存映射区中的内容一致,则调用msync来 执行这种同步。
其中addr和len参数通常指代内存中的整个内存映射区,不过也可指定该内存区的一个子集。flags是如下常值的组合:
MS_ASYNC和MS_SYNC这两个值必须指定一个,但不能都指定。两者的差别是,一旦写操作已由内核排入队列,MS_ASYNC即返回,而 MS_SYNC则要等到写操作完成后返回。如果还指定了MS_INVALIDATE,那么与其最终拷贝不一致的文件数据的所有内存中拷贝都失效,后续的引 用将从文件中取得数据。
如果调用mmap的目的是提供一个即将穿越fork由父子进程共享的映射内存区,则可以不用创建一个文件,在open它等一系列操作。具体依赖于实现: