1、管道
管道的局限性:
(1)历史上,它们是半双工的(即数据只能在一个方向上流动)。
(2)管道只能在具有公共祖先的两个进程之间使用。
半双工管道是最常用的IPC形式。每当在管道中键入一个命令序列,让shell执行时,shell都会为每一条命令单独创建一个进程,然后用管道将前一条命令进程的标准输出与后一条命令的标准输入相连接。
通过调用pipe创建管道:
#include <unistd.h>
int pipe(int fd[2]);
//若成功,返回0;若出错,返回-1;
经由参数fd返回两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。fd[1]的输出是fd[0]的输入。
当管道的一端被关闭后:
(1)当读(read)一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,表示文件结束。
(2)如果写(write)一个读端已被关闭的管道,则产生信号SIGPIPE。
2、函数popen和pclose
创建一个管道,fork一个子进程,关闭未使用的管道端,执行一个shell运行命令,然后等待命令终止:
#include <stdio.h>
FILE *popen(const char *cmdstring,const char *type);
//若成功,然会文件指针;若出错,返回NULL
int pclose(FILE *fp);
//若成功,返回cmdstring的终止状态;若出错,返回-1
函数popen先执行fork,然后调用exec执行cmdstring,并且返回一个标准I/O文件指针。如果type是“r”,则文件指针连接到cmdstring的标准输出;如果type是”w”,则文件指针连接到cmdstring的标准输入。
pclose函数关闭标准I/O流,等待命令终止,然后返回shell的终止状态。
3、协同进程
当一个过滤程序既产生某个过滤程序的输入,又读取该过滤程序的输出时,它就变成了协同进程。协同进程通常在shell的后台运行,其标准输入和标准输出通过管道连接到另一个程序。
popen只提供连接到另一个进程的标准输入或标准输出的一个单向管道,而协同进程则由连接到另一个进程的两个单向管道,一个接到其标准输入,另一个则来自其标准输出。
4、FIFO
FIFO有时被称为命名管道。未命名的管道只能在两个相关的进程之间使用,而且这两个相关的进程还要有一个共同的创建了它们的祖先进程。但是,通过FIFO,不相关的进程也能交换数据。
FIFO是一种文件类型。创建FIFO类似于创建文件:
#include <sys/stat.h>
int mkfifo(const char *path,mode_t mode);
int mkfifoat(int fd,const char *path,mode_t mode);
//若成功,返回0;若出错,返回-1;
当我们用mkfifo或者mkfifoat创建FIFO时,要用open来打开它。当open一个FIFO时,非阻塞标志(O_NONBLOCK)会产生下列影响:
- 在一般情况下(没有指定O_NONBLOCK),只读open要阻塞到某个其他进程为写而打开这个FIFO位置。类似地,只写open要阻塞到某个其他进程为读而打开它为止。
- 如果指定了O_NONBLOCK,则只读open立即返回。但是,如果没有进程为读而打开一个FIFO,那么只写open将返回-1,并将errno设置称ENXIO。
类似与管道,若write一个尚无进程为读而打开的FIFO,则产生信号SIGPIPE。若某个FIFO的最后一个写进程关闭了该FIFO,则将为该FIFO的读进程禅僧一个文件结束标志。
FIFO有以下两种用途:
(1)shell命令使用FIFO将数据从一条管道传送到另一条时,无需创建中间临时文件。
(2)客户进程-服务器进程应用程序中,FIFO用作汇聚点,在客户进程和服务器进程二者之间传递数据。
5、XSI IPC
有3种称作XSI IPC的IPC:消息队列、信号量以及共享存储器。
5.1、标识符和键
每个内核中的IPC结构(消息队列、信号量或共享存储段)都用一个非负整数的标识符加以引用。标识符是IPC对象的内部名。每个IPC对象都与一个键(key)相关联,将这个键作为该对象的外部名。
有多种方法使客户进程和服务器进程在同一IPC结构上汇聚:
(1)服务器进程可以指定键IPC_PRIVATE创建一个新IPC结构,将返回的标识符存放在某处(如一个文件)以便客户进程取用。键IPC_PRIVATE保证服务器进程创建一个新IPC结构。
(2)可以在一个公用头文件中定义一个客户进程和服务器进程都认可的键。然后服务器进程指定此键创建一个新的IPC结构。
(3)客户进程和服务器进程认同一个路径名和项目ID,接着,调用函数ftok将这两个值变换为一个键。然后在方法(2)中使用此键。
调用函数ftok可将一个路径名和项目ID变换为一个键:
#include <sys/ipc.h>
key_t ftok(const char *path,int id);
若成功,返回键;若出错,返回(key_t)-1
在创建新的IPC结构(通常由服务器进程创建)时,如果key是IPC_PRIVATE或者和当前某种类型的IPC结构无关,则需要指明flag的IPC_CREAT标志位。注:决不能指定IPC_PRIVATE作为键来引用一个现有队列,因为这个特殊的键值总是用于创建一个新队列。
5.2、权限结构
XSI IPC为每一个IPC结构关联了一个ipc_perm结构,该结构规定了权限和所有者:
struct ipc_perm{
uid_t uid; /*owner's effective user id */
gid_t gid; /*owner's effective group id */
uid_t cuid; /*creator's effective user id */
gid_t cgid; /*craetor's effective group id */
mode_t mode; /*access modes*/
}
6、消息队列
消息队列是消息的链接表,存储在内核中,由消息队列标识符标识。
每个队列都有一个msqid_ds结构与其相关联:
struct msqid_ds {
struct ipc_perm msg_perm;
msgqnum_t msg_qnum; /*# of messages on queue*/
msglen_t msg_qbytes; /*max # of bytes on queue*/
pid_t msg_lspid; /*pid of last msgsnd()*/
pid_t msg_lrpid; /*pid of last msgrcv()*/
time_t msg_stime; /*last-magsnd() time*/
time_t msg_rtime; /*lat-msgrcv() time*/
time_t msg_ctime; /*last-change time*/
};
调用的第一个函数通常是msgget,其功能是打开一个现有队列或创建一个新队列:
#include <sys/msg.h>
int msgget(key_t key,int flag);
//若成功,返回消息队列ID,若出错,返回-1
在创建新队列时,要初始化msqid_ds结构的成员。
- ipc_perm结构中的mode成员按flag中的相应权限位设置。这些权限用图15-24中的值指定。
- msg_qnum、msg_lspid、msg_lrpid、msg_stime和msg_rtime都设置为0.
- msg_ctime设置为当前时间。
- msg_qbytes设置为系统限制值。
msgctl函数对队列执行多种操作:
#include <sys/msg.h>
int msgctl(int msqid,int cmd,struct msqid_da *buf);
//若成功,返回0;若出错,返回-1
cmd参数指定对msqid指定的队列要执行的命令:
IPC_STAT:取此队列的msqid_ds结构,并将它存放在buf指向的结构中。
IPC_SET:将字段msg_perm.uid、msg_perm.gid、msg_perm.mode和msg_qbytes从buf指向的结构复制到与这个队列相关的msqid_ds结构中。此命令只能由下列两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid。另一种是具有超级用户特权的进程。只有超级用户才能增加msg_qbytes的值。
IPC_RMID:从系统中删除该消息队列以及仍在该队列中的所有数据。此命令只能由下列两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid。另一种是具有超级用户特权的进程。
调用msgsnd将数据放到消息队列中:
#include <sys/msg.h>
int msgsnd(int msqid,const void *ptr,size_t nbytes,int flag);
//若成功,返回0;若出错,返回-1
每个消息都由3部分组成:一个正的长整型类型的字段、一个非负的长度(nbytes)以及实际数据字节数。
ptr参数指向一个长整型数,它包含了正的整型消息类型,其后紧接着的事消息数据(若nbytes是0,则无消息数据)。若发送的最长消息是512字节的,则可定义下列结构:
struct mymesg{
long mtype;
char mtext[512];
};
ptr就是一个指向mymesg结构的指针。接收者可以使用消息类型以非先进先出的次序取消息。
参数flag的值可以指定为IPC_NOWAIT。若消息队列已满,则指定IPC_NOWAIT使得msgsnd立即出错返回EAGAIN。如果没有指定IPC_NOWAIT,则进程会一直阻塞到:有空间可以容纳要发送的消息;或者从系统中删除了此队列(返回EIDRM错误);或者捕捉到一个信号,并从信号处理程序返回(返回EINTR错误)。
msgrcv从队列中取用消息:
#include <sys/msg.h>
ssize_t msgrcv(int msqid,void *ptr,size_t nbytes,long type,int flag);
//若成功,返回消息数据部分的长度;若出错,返回-1
参数type可以指定想要哪一种消息:
type==0:返回队列中的第一个消息。
type>0:返回队列中消息类型为type的第一个消息。
type<0:翻译队列中消息类型值小于等于type绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。
7、信号量
信号量是一个计数器,主要用于保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。
为了获得共享资源,进程需要执行下列操作:
(1)测试控制该资源的信号量。
(2)若此信号量的值为正,则进程可以使用该资源。在这种情况下,进程会将信号量值减1,表示它使用了一个资源单位。
(3)否则,若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0。进程被唤醒后,返回至步骤(1)。
内核为每个信号量集合维护者一个semid_ds结构:
struct semid_ds{
struct ipc_perm sem_perm;
unsigned short sem_nsems; /*# of semaphores in set*/
time_t sem_otime; /*last-semop() time*/
time_t sem_ctime; /*last-change time*/
};
每个信号量由一个无名结构表示:
struct {
unsigned short semval; /*semaphore value,always>=0*/
pid_t sempid; /*pid for last operation*/
unsigned short semncnt;/*# processes awaiting semval>curval*/
unsigned short semzcnt;/*# processes awaiting semval==0*/
};
当我们想使用XSI信号量时,首先需要通过调用函数semget来获得一个信号量ID:
#include <sys/sem.h>
int semget(key_t key,int nsems,int flag);
//若成功,返回信号量ID;若出错,返回-1
创建一个新集合时,要对semid_ds结构的下列成员赋初值:
- 初始化ipc_perm结构。该结构中的mode成员被设置称flag中的相应权限位。
- sem_otime设置为0.
- sem_ctime设置为当前时间。
- sem_nsems设置为nsems。
nsems是该集合中的信号量数。如果是创建新集合(一般在服务器进程中),则必须指定nsems。如果是引用现有集合,则将nsems指定为0。
semctl函数包含了多种信号量操作:
#include <sys/sem.h>
int semctl(int semid,int semnum,int cmd,.../* union semun arg */);
union semun {
int val; /*for SETVAL*/
struct semid_ds *buf; /*for IPC_STAT and IPC_SET*/
unsigned short *array;/*for GETALL and SETALL*/
};
semnum参数指定该信号量集合中的一个成员。semnum值在0和nsems-1之间。
cmd参数:
IPC_STAT: 对此集合取semid_ds结构,并将它存放在arg.buf指向的结构中。
IPC_SET: 按arg.buf指向的结构的值,设置与此集合相关的结构中的sem_perm.uid、sem_perm.gid和sem_perm.mode。此命令只能由下列两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid。另一种是具有超级用户特权的进程。
IPC_RMID:从系统中删除该信号量集合。此命令只能由下列两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid。另一种是具有超级用户特权的进程。
GETVAL:返回成员semnum的semval值。
SETVAL:设置成员semnum的semval值。该值由arg.val指定。
GETPID:返回成员semnum的sempid值。
GETNCNT:返回成员semnum的semncnt值。
GETZCNT:返回成员semnum的semzcnt值。
GETALL:取该集合中所有的信号量值。这些值存储在arg.array指向的数组中。
SETALL:将该集合中所有的信号量值设置成arg.array指向的数组中的值。
对于除GETALL以外的所有GET命令,semctl函数都返回相应值。对于其他命令,若成功,则返回值0,若出错,则设置errno并返回-1.
函数semop自动执行信号量集合上的操作数组:
#include <sys/sem.h>
int semop(int semid,struct sembuf semoparray[],size_t nops);
//若成功,返回0;若出错,返回-1
参数semoparray指向一个由sembuf结构表示的信号量操作数组:
struct sembuf {
unsigned short sem_num; /*member # in set(0,1,...,nsems-1*/
short sem_op; /*operation(negative,0 or pasitive*/
short sem_flg; /*IPC_NOWAIT,SEM_UNDO*/
};
对集合中每个成员的操作由相应的sem_op值规定:
(1)sem_op为正值。这对应于进程释放的占有的资源数。sem_op值会加到信号量的值上。
(2)sem_op为负值,则表示要获取由该信号量控制的资源。
如若该信号量的值大于等于sem_op的绝对值(具有所需的资源),则从信号量值中减去sem_op的绝对值。
如果信号量值小于sem_op的绝对值(资源不能满足要求),则:
a.若指定了IPC_NOWAIT,则semop出错返回EAGAIN。
b.若未指定IPC_NOWAIT,则该信号量的semncnt值加1,然后调用进程被挂起直至下列事件之一发生:
i.此信号量值变成大于等于sem_op的绝对值(即某个进程已释放了某些资源)。此信号量的semncnt值键1,并且从信号量值中减去sem_op的绝对值。
ii.从系统中删除了此信号量。在这种情况下,函数出错返回EIDRM。
iii.进程捕捉到一个信号,并从信号处理程序返回,在这种情况下,此信号量的semncnt值减1,并且函数出错返回EINTR。
8、共享存储
共享存储允许两个或多个进程共享一个给定的存储区。通常,信号量用于同步共享存储访问。
内核为每个共享存储段维护着一个结构:
#struct shmid_ds {
struct ipc_perm shm_perm;
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;/*number of current attaches*/
time_t shm_atime; /*last-attach time*/
time_t shm_dtime; /*last-detach time*/
time_t shm_ctime; /*last-change time*/
};
调用的第一个函数通常是shmget,它获得一个共享存储标识符:
#include <sys/shm.h>
int shmget(key_t key,size_t size,int flag);
//若成功,返回共享存储ID;若出错,返回-1
创建一个新段时,要对shmid_ds结构的下列成员初始化:
- 初始化ipc_perm结构。该结构中的mode成员被设置称flag中的相应权限位。
- shm_lpid、shm_nattach、shm_atime和shm_dtime设置为0.
- shm_ctime设置为当前时间。
- shm_segsz设置为请求的size。
参数size是 该共享存储段的长度,以字节为单位。如果正在创建一个新段(通常在服务器进程中),则必须指定其size。如果正在引用一个现存的段(一个客户进程),则将size指定为0.
shmctl函数对共享存储段执行多种操作:
#include <sys/shm.h>
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
//若成功,返回0;若出错哦,返回-1
cmd参数:
IPC_STAT: 取此段的shmid_ds结构,并将它存储在由buf指向的结构中。
IPC_SET: 按buf指向的结构的值,设置与此共享存储段相关的shmid_ds结构中的shm_perm.uid、shm_perm.gid和shm_perm.mode。此命令只能由下列两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid。另一种是具有超级用户特权的进程。
IPC_RMID:从系统中删除该共享存储段。因为每个共享存储段维护这一个连接技数(shmid_ds结构中的shm_nattch字段),所以除非使用该段的最后一个进程终止或与该段分离,否则不会实际上删除该存储段。此命令只能由下列两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid。另一种是具有超级用户特权的进程。
一旦创建了一个共享存储段,进程就可调用shmat将其连接到它的地址空间中:
#include <sys/shm.h>
void *shmat(int shmid,const void *addr,int flag);
//若成功,返回指向共享存储段的指针;若出错,返回-1
- 如果addr为0,则此段连接到由内核选择的第一个可用地址上(推荐)。
- 如果addr非0,并且flag中没有指定SHM_RND,则此段连接到addr所指定的地址上。
- 如果addr非0,并且指定了SHM_RND,则此段连接到(addr-(addr mod SHMLBA)所表示的地址上。
如果在flag中指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写方式连接此段。
shmat的返回值是该段所连接的实际地址,如果出错则返回-1.如果shmat成功执行,那么内核将使与该共享存储段相关的shmid_ds结构中的shm_nattch计数器值加1.
当对共享存储段的操作已经结束时,则调用shmdt与该段分离。
#include <sys/shm.h>
int shmdt(const void *addr);
//若成功,返回0;若出错,返回-1
如果成功,shmdt将使相关shmid_ds结构中的shm_nattch计数器值减1。
9、POSIX信号量
POSIX信号量有两种形式:命名的和未命名的。未命名信号量只存在与内存中,并要求能使用信号量的进程必须可以访问内存。这意味着它们智能应用在同一进程中的线程,或者不同进程中已经映射相同内存内容到它们的地址空间中的线程。命名信号量可以通过名字访问,因此可以被任何已知它们名字的进程中的线程使用。
创建一个新的命名信号量或者使用一个现有信号量:
#include <semaphore.h>
sem_t *sem_open(const char *path,int oflag,.../* mode_t mode,unsigned int value*/);
//若成功,返回指向信号量的指针,若出错,返回SEM_FAILED
当使用一个现有的命名信号量时,我们仅仅指定两个参数:信号量的名字和oflag参数的0值。当这个oflag参数由O_CREAT标志集时,如果命名信号量不存在,则创建一个新的。
在创建信号量时,value参数用来指定信号量的初始值,它的取值是0~SEM_VALUE_MAX。
当完成信号量操作时,可以调用sem_close函数来释放任何信号量相关的资源。
#include <semaphore.h>
int sem_close(sem_t *sem);
//若成功,返回0;若出错,返回-1
注:调用sem_close,信号量值不会受到影响。
使用sem_unlink函数来销魂一个命名信号量:
#include <semaphore.h>
int sem_unlink(const char *name);
//若成功,返回0;若出错,返回-1
sem_unlink函数删除信号量的名字。
使用sem_wait或者sem_trywait函数来实现信号量的减1操作:
#include <semaphore.h>
int sem_trywait(sem_t *sem);
int sem_wait(sem_t *sem);
//若成功,返回0;若出错,返回-1
使用sem_wait函数时,如果信号量计数是0就会发生阻塞。知道成功使信号量减1或者被信号中断时才返回。调用sem_wait时,如果信号量是0,则不会阻塞,而是返回-1并将errno置为EAGAIN。
阻塞一段时间:
#include <semaphore.h>
#include <time.h>
int sem_timedwait(sem_t *restrict sem,const struct timespec *restrict tsptr);
//若成功,返回0;若出错,返回-1
sem_post函数使信号量增1:
#include <semaphore.h>
int sem_post(sem_t *sem);
//若成功,返回0;若出错,返回-1
调用sem_post时,如果在调用sem_wait中发生进程阻塞,那么进程会被唤醒并且被sem_post增1的信号量计数会再次被sem_wait减1.
当我们想在单个进程中使用POSIX信号量时,使用未命名信号量更容易。创建一个未命名的信号量:
#include <semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigned int value);
//若成功,返回0;若出错,返回-1
pshared参数表明是否在多个进程中使用信号量,如果是,则设置成一个非0值。
对未命名信号量的使用已经完成时,可以调用sem_destroy函数丢弃它:
#include <semaphore.h>
int sem_destroy(sem_t *sem);
//若成功,返回0;若出错,返回-1
sem_getvalue函数可以用来检索信号量值:
#include <semaphore.h>
int sem_getvalue(sem_t *restrict sem,int *restrict valp);
//若成功,返回0;若出错,返回-1