《UNIX环境高级编程》啃书笔记(第15章进程间通信)

进程间通信(InterProcess Communication,IPC)

管道

管道是半双工的,且只能在具有公共祖先的两个进程之间使用。通常一个管道由一个进程创建,在进程调用fork之后,这个管道就能在父进程与子进程之间使用了。

每当在管道中键入一个命令序列,让shell执行时,shell都会为每一条命令单独创建一个进程,然后用管道将前一条命令进程的标准输出与后一条命令的标准输入相连接。

管道是通过调用pipe函数创建的:

#include<unistd.h>
int pipe(int fd[2]);

若成功返回0,否则出错返回-1。

经由参数fd返回两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。fd[1]的输出是fd[0]的输入,但数据需要通过内核在管道中流动。fstat函数对管道的每一端都返回一个FIFO类型的文件描述符,可以用S_ISFIFO宏来测试管道。

文件描述符通常是一个小的非负整数,内核用以标识一个特定进程正在访问的文件。

单个进程中的管道几乎没有任何用处。通常,进程会先调用pipe,接着调用fork,从而创建从父进程到子进程的IPC通道,反之亦然。fork之后做什么取决于我们想要的数据流的方向。对于从父进程到子进程的管道,父进程关闭管道的读端(fd[0]),子进程关闭写端(fd[1])。

当管道的一端被关闭后,有如下两规则:

  1. 当读(read)一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,表示文件结束。
  2. 若写(write)一个读端已被关闭的管道,则产生信号SIGPIPE。若忽略该信号或者捕捉该信号并从其处理程序返回,则write返回-1,errno设置为EPIPE。在写管道(或FIFO)时,常量PIPE_BUF规定了内核的管道缓冲区大小。

通常一个管道只有一个读进程和一个写进程。下例为创建一个从父进程到子进程的管道,并且父进程经由该管道向子进程传送数据:

int main(void){
    int n;
    int fd[2];
    pid_t pid;
    char line[MAXLINE];

    if(pipe(fd)<0) err_sys("pipe error");

    if((pid=fork())<0) err_sys("fork error");
    else if(pid>0){      //父进程关闭读端,并写写端写入数据
        close(fd[0]);
        write(fd[1],"hello world\n",12);
    }else{              //子进程关闭写端,并读入读端传送过来的数据
        close(fd[1]);
        n=read(fd[0],line,MAXLINE);
        write(STDOUT_FILENO,line,n);
    }
    exit(0);
}

函数popen和pclose

使用管道时常见的操作是创建一个连接到另一个进程的管道,然后读其输出或向其输入端发送数据,为此,标准I/O库提供了两个函数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的终止状态。若shell不能被执行,则pclose返回的终止状态与shell已执行exit(127)一样。

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。

参数mode表示文件的访问权限。

mkfifoat可以被用来在fd文件描述符表示的目录相关的位置创建一个FIFO:

  1. 若path参数指定的是绝对路径名,则fd参数会被忽略,且mkfifoat函数的行为和mkfifo类似
  2. 若path参数指定的是相对路径名,则fd参数是一个打开目录的有效文件描述符,路径名和目录有关
  3. 若path参数指定的是相对路径名,且fd参数有一个特殊值AT_FDCWD,则路径名以当前目录开始,mkfifoat和mkfifo类似

FIFO有以下两种用途:

  1. shell命令使用FIFO将数据从一条管道传送到另一条时,无需创建中间临时文件
  2. 客户进程-服务器进程应用程序中,FIFO用作汇聚点,在客户进程和服务器进程二者之间传递数据

下图表示用FIFO进行客户进程-服务器进程通信:
用FIFO进行客户进程-服务器进程通信

这里每个客户进程都在其请求中包含它的进程ID,然后服务器进程为每个客户进程创建一个FIFO,所使用的路径名是以客户进程的进程ID为基础。但这种方式服务器进程不能判断一个客户进程是否奔溃终止,导致客户进程专用FIFO会遗留在文件系统中,避免这种问题的一种常用技巧是使服务器进程以读-写方式打开该总所周知的FIFO。

XSI IPC相似特征

XSI IPC包括消息队列、信号量以及共享存储器。

标识符和键

每个内核中的IPC结构(消息队列、信号量或共享存储段)都用一个非负整数的标识符加以引用。当一个IPC结构被创建,然后又被删除时,与这种结构相关的标识符连续加1,直至达到一个整型数的最大正值,然后又回转到0。

标识符是IPC对象的内部名。为使多个合作进程能够在同一IPC对象上汇聚,每个IPC对象都与一个键(key)相关联,将这个键作为该对象的外部名。无论何时创建IPC结构,都应指定一个键,这个键的数据类型是key_t,一般被定义为长整型,这个键由内核变换成标识符。

以下方法使客户进程和服务器进程在同一IPC结构上汇聚:

  1. 服务器进程指定键IPC_PRIVATE创建一个新IPC结构,将返回的标识符存放在某处(如文件)以便客户进程取用。键IPC_PRIVATE保证服务器进程创建一个新IPC结构。
  2. 可以在一个公用头文件中定义一个客户进程和服务器进程都认可的键,然后服务器进程指定此键创建一个新的IPC结构。
  3. 客户进程和服务器进程认同一个路径名和项目ID(0~255间字符值),接着调用函数ftok将这两个值变换为一个键。然后再上面方法2中使用此键。ftok提供的唯一服务就是由一个路径名和项目ID产生一个键:
#include<sys/ipc.h>
key_t ftok(const char *path,int id);

若成功返回键,出错返回(key_t)-1。

path参数必须引用一个现有的文件,当产生键时,只使用id参数的低8位。

ftok创建的键:按给定的路径名取得其stat结构中的st_dev和st_ino字段,然后再将它们与项目ID组合起来。

3个get函数都有两个类似参数:key和整型flag。在创建新的IPC结构时,若key是IPC_PRIVATE或者和当前某种类型的IPC结构无关,则需要指明flag的IPC_CREAT标志位。为了引用一个现有队列,key必须等于队列创建时指明的key的值,并且IPC_CREAT必须不被指明。

注意决不能指定IPC_PRIVATE作为键来引用一个现有队列,因为这个特殊的键值总是用于创建一个新队列。若希望创建一个新的IPC结构,而且要确保没有引用具有同一标识符的一个现有IPC结构,那么必须在flag中同时指定IPC_CREAT和IPC_EXCL位,这样若IPC结构已存在就会出错返回EEXIST。

优点和缺点

XSI IPC的一个基本问题在于:IPC结构是在系统范围内起作用的,没有引用计数。例如,若进程创建了一个消息队列,且在该队列中放入了几则消息,然后终止,那么该消息队列及其内容都不会被删除,它们会一直留在系统中直至发生下列动作:由某个进程调用msgrcv或msgctl读消息或删除消息队列,或某个进程执行ipcrm(1)命令删除消息队列,或正在自举的系统删除消息队列。

对于管道:当最后一个引用管道的进程终止时,管道就被完全的删除了
对于FIFO:在最后一个引用FIFO的进程终止时,虽然FIFO的名字仍保留在系统中,直至被显式删除,但留在FIFO中的数据已被删除了。

另一个问题在于这些IPC结构在文件系统中没有名字,不使用文件描述符,所以不能对它们使用多路转接I/O函数。

消息队列

消息队列是消息的链接表,存储在内核中,由消息队列标识符标识(简称队列ID)。消息队列的速度高于管道和FIFO。

msgget用于创建一个新队列或打开一个现有队列,msgsnd将新消息添加到队列尾端。每个消息包含一个正的长整型类型的字段、一个非负的长度以及实际数据字节数(对应于长度),所有这些都在将消息添加到队列时,传送给msgsnd。msgrcv用于从队列中取消息。并非一定要以先进先出次序取消息,也可以按消息的类型字段取消息。每个队列都有一个msqid_ds结构与其关联,此结构定义了队列的当前状态:

struct msqid_ds{
    struct ipc_perm msg_perm;    //权限结构,规定了权限和所有者
    msgqnum_t msg_qnum;          //消息队列数目
    msglen_t msg_qbytes;         //队列数据字节数,即长度
    pid_t msg_lspid;             //队列尾端消息标识符
    pid_t msg_lrpid;             //队列首端消息标识符
    time_t msg_stime;            //最后一次添值时间
    time_t msg_rtime;            //最后一次取值时间
    time_t msg_ctime;            //最后一次修改时间
    ...
};

调用的第一个函数通常是msgget,其功能是打开一个现有队列或创建一个新队列:

#include<sys/msg.h>
int msgget(key_t key,int flag);

若成功返回消息队列ID,出错返回-1。

key根据上章标识符和键中所述规则赋值。在创建新队列时,要初始化msqid_ds结构的下列成员:

  • ipc-perm结构,其中mode成员按flag中的相应权限位设置
  • msg_qnum、msg_lspid、msg_lrpid、msg_stime和msg_rtime都设置为0
  • msg_ctime设置为当前时间
  • msg_qbytes设置为系统限制值

若执行成功,msgget返回非负队列ID,此后该值就可被用于其他3个消息队列函数。

msgctl函数对队列执行多种操作:

#include<sys/msg.h>
int msgctl(int msqid,int cmd,struct msqid_ds *buf);

若成功返回0,出错返回-1。

cmd参数指定对msqid指定的队列要执行的命令:

IPC_STAT 取此队列的msqid_ds结构,并将它存放在buf指向的结构中
IPC_SET 将msg_perm结构中的字段uid、gid、mode和字段msg_qbytes从buf指向的结构复制到与这个队列相关的msqid_ds结构中。
IPC_RMID 从系统中删除该消息队列以及仍在该队列中的所有数据,这种删除立即生效

后两个命令只允许两种进程执行:有效用户ID等于msg_perm中的cuid或uid;或者是具有超级用户特权的进程。且只有超级用户才能增加msg_qbytes的值。

这3条命令也可用于信号量和共享存储。

调用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错误。

当msgsnd返回成功时,消息队列相关的msqid_ds结构会随之更新,表明调用的进程ID(msg_lspid)、调用的时间(msg_stime)以及队列中新增的消息(msg_qnum)。

msg_rcv从队列中取用消息:

#include<sys/msg.h>
ssize_t msgrcv(int msqid,void *ptr,size_t nbytes,long type,int flag);

若成功返回消息数据部分的长度,若出错返回-1。

ptr参数指向一个长整型(其中存储的是返回的消息类型),其后跟随的是存储实际消息数据的缓冲区。nbyte指定数据缓冲区的长度。若返回的消息长度大于nbytes,而且在flag中设置了MSG_NOERROR位,则该消息会被截断(此时并没有通知消息被截去的部分被丢弃了)。若没有设置这一标志,而消息又太长,则出错返回E2BIG(消息仍留在队列中)。

参数type可以指定想要哪一种消息:

type==0 返回队列中的第一个消息
type>0 返回队列中消息类型为type的第一个消息
type<0 返回队列中消息类型值小于等于type绝对值的消息,若有多个取类型值最小的消息

type值非0用于以非先进先去次序读消息。例如用type表示优先权值,或多客户进程的服务器中type可以用来包含客户进程的进程ID。

可以将flag值指定为IPC_NOWAIT使操作不阻塞,这样若没有指定类型的消息可用,则msgrcv返回-1,error设置为ENOMSG。若没有指定IPC_NOWAIT,则进程会一直阻塞到有了指定类型的消息可用,或者从系统中删除了此队列(返回-1,error设置为EIDRM),或者捕捉到一个信号并从信号处理程序返回(返回-1,error设置为EINTR)。

msgrcv成功执行时,内核会更新与该消息队列相关联的msgid_ds结构,以指示调用者的进程ID(msg_lrpid)和调用时间(msg_rtime),并指示队列中的消息数减少了1个(msg_qnum)。

信号量

信号量是一个计数器,用于为多个进程提供对共享数据对象的访问。

信号量的速度比记录锁和互斥量慢。其中互斥量速度最快但未得到普遍支持。

为了获得共享资源,进程需要执行下列操作:

  1. 测试控制该资源的信号量
  2. 若为正,进程可使用,信号量值减1,表示它使用了一个资源单位
  3. 若为0,进程休眠直至值大于0,进程被唤醒后返回1

当进程不再使用一个信号量控制的共享资源时,该信号量增1,若此时有进程正休眠等待此信号量,唤醒它们。一般而言信号量初值为任意正数表有多少个共享资源单位可供共享应用。

内核为每个信号量维护着一个semid_ds结构:

struct semid_ds{
    struct ipc_perm sem_perm;       //权限结构,规定了权限和所有者
    unsigned short sem_nsems;       //信号量数目
    time_t sem_otime;               //最后一次调用semop()的时间
    time_t sem_ctime;               //最后一次修改时间
};

每个信号量为一无名结构:

struct {
    unsigned short semval;        //信号量值,永远>=0
    pid_t sempid;                 //pid for last operation
    unsigned short semncnt;       //正休眠的进程数目,semval>curval资源不能满足要求
    unsigned short semzcnt;       //正休眠的进程数目,semval=0,无可用资源
};

首先通过调用semget函数来获得一个信号量ID:

#include<sys/sem.h>
int semget(key_t key,int nsems,int flag);

若成功返回信号量ID,出错返回-1。

当创建一个新集合时,要对semid_ds结构的下列成员赋初值:

  • 初始化ipc_perm结构
  • 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 srg);

第4个参数是可选的,是否使用取决于所请求的命令,若使用该参数,则其类型是semun,它是多个命令特定参数的联合(是联合非指向联合的指针):

union semun{
    int val;  //for SETVAL
    struct semid_ds *buf; //for IPC_STAT and IPC_SET
    unsigned short *array; //for GETALL and SETALL
};

cmd参数指定下列10种命令中的一种,这些命令是运行在semid指定的信号量集合上的。其中针对一个特定的信号量值的命令用semnum指定该信号量集合中的一个成员——semnum值在0和nsems-1之间:

IPC_STAT 对此集合取semid_ds结构并存储在由arg.buf指向的结构中
IPC_SET 按arg.buf指向的结构中的值,设置与此集合相关的结构中的sem_perm的uid、gid、mode字段
IPC_RMID 从系统中删除该信号量集合,立即生效
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,若出错返回-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;    //信号量集合中的编号
    short sem_op;    //成员的操作方式
    short sem_flg;   //IPC_NOWAIT,SEM_UNDO
};

参数nops规定该数组中操作的数量(元素数)。

对集合中每个成员的操作由相应的sem_op值规定,此值为负值、0或正值:

  1. sem_op>0:表示进程释放的资源数,sem_op会加到信号量的值上,若指定了undo标志则也从该进程中的该信号量调整值中减去sem_op。
  2. sem_op<0:表示要获取由该信号量控制的资源。
    1. 该信号量值大于sem_op的绝对值(具有所需的资源),则从信号量值中减去sem_op的绝对值。若指定了undo标志,则sem_op的绝对值也加到该进程的此信号量调整值上。
    2. 若信号量值小于sem_op的绝对值(资源不能满足要求),则适用下列条件:
      1. 若指定了IPC_NOWAIT,则semop出错返回EAGAIN
      2. 若未指定,则该信号量的semncnt值加1(调用进程将进入休眠状态),然后调用进程被挂起直至信号量大于等于sem_op的绝对值,semncnt值减1,跳转信号量大于时的处理;但此时
        1. 若从系统中删除了此信号量则函数出错返回EIDRM;
        2. 若进程捕捉到一个信号,并从信号处理程序返回,此时调用进程不再等待,semncnt减1,函数出错返回EINTR
  3. 若sem_op==0:表示调用进程希望等待到该信号量值为0
    1. 信号量为0,函数立即返回
    2. 信号量非0:
      1. 若指定IPC_NOWAIT,出错返回EAGAIN
      2. 若未指定该信号量的semzcnt值加1,然后调用进程休眠被挂起直至此信号量变为0,结束等待,semzcnt值减1;但此时:
        1. 若从系统中删除了此信号量则函数出错返回EIDRM;
        2. 若进程捕捉到一个信号,并从信号处理程序返回,此时调用进程不再等待,semzcnt减1,函数出错返回EINTR

semop函数具有原子性,它或者执行数组中的所有操作,或者一个也不做。

即使没有进程正在使用XSI IPC,它们仍是存在的,有程序在终止时并没有释放已经分配给它的信号量,所有我们使用undo功能:
无论何时只要为信号量操作指定了SEM_UNDO标志,然后分配资源(sem_op值小于0),那么内核就会记住对于该特定信号量,分配给调用进程多少资源(sem_op的绝对值)。当该进程终止时,不论自愿或者不自愿,内核都将检验该进程是否还有尚未处理的信号量调整值,若有则按调整值对相应信号量进行处理。

共享存储

共享存储允许两个或多个进程共享一个给定的存储区。由于数据无需在客户进程与服务器进程之间复制故而这是最快的一种IPC。通常信号量、记录锁、互斥量都能用于同步共享存储访问,当然速度上互斥量>记录锁>互斥量。

XSI共享存储段是内存的匿名段,它不同于内存映射的文件—也就是没有相关的文件。

内核为每个共享存储段维护着一个shmid_ds的结构(太长懒得写了),当创建一个新段时都要求初始化。

shmget函数获得一个共享存储标识符:

#include<sys/shm.h>
int shmget(key_t key,size_t size,int flag);

若成功返回共享存储ID,出错返回-1。

参数size是该共享存储段的长度,以字节为单位,通常将其向上取为系统页长的整数倍,否则最后一页余下部分是不可使用的。若是创建一个新段则必须指定其size,若为引用一个现存的段则将size指定为0。当创建一个新段时,段内的内容初始化为0。

shmctl函数对共享存储段执行多种操作:

#include<sys/shm.h>
int shmctl(int shmid,int cmd,struct shmid_ds *buf);

若成功返回0,出错返回-1。

cmd参数取5种命令之一,使其在shmid指定的段上执行:

IPC_STAT 取此段的shmid_ds结构,并将它存储在由buf指向的结构中
IPC_SET 按buf指向的结构中的值设置与此共享存储段相关的uid、gid、mode
IPC_RMID 从系统中删除该共享存储段,因为每个共享存储段维护着一个连接计数,故除非使用该段的最后一个进程终止或与该段分离,否则不会实际上删除该存储段。不过不管此段是否仍在使用,该段标识符都会被立即删除,所有不能再用shmat与此段连接
SHM_LOCK 在内存中对共享存储段加锁,只允许超级用户执行
SHM_UNLOCK 解锁共享存储段,只允许超级用户执行

一旦创建了一个共享存储段,进程就可调用shmat将其连接到它的地址空间中:

#include<sys/shm.h>
void *shmat(int shmid,const void *addr,int flag);

若成功返回指向共享存储段的指针,出错返回-1。

共享存储端连接到调用进程的哪个地址上与addr参数已经flag中是否指定SHM_RND位有关:

  • addr为0:此段连接到由内核选择的第一个可用地址上。推荐使用
  • addr非0且没有指定SHM_RND:此段连接到addr所指定的地址上
  • addr非0且指定SHM_RND:此段连接到(addr-(addr mod SHMLBA))所表示的地址上。SHM_RND命令的意思是取整,SHMLBA的意思是低边界地址倍数,它总是2的乘方,该算式是将地址向下取最近一个SHMLBA的倍数。

若在flag中指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写方式连接此段。

shmat的返回值是该段所连接的实际地址,若出错则返回-1。若shmat成功执行,那么内核将使与该共享存储段相关的shmid_ds结构中的shm_nattch计数器加1。

当对共享存储段的操作已经结束时,则调用shmdt与该段分离,注意这并不从系统中删除其标识符及其相关的数据结构,该标识符仍然存在直至某个进程待IPC_RMID命令的调用shmctl特地删除它为止:

#include<sys/shm.h>
int shmdt(const void *addr);

若成功返回0,出错返回-1。

addr参数是以前调用shmat时的返回值,若成功shmdt将使相关shmid_ds结构中的shm_nattch计数器值减1。

下图为基于Intel的Linux系统上的存储区布局:
基于Intel的Linux系统上的存储区布局

更优秀的POSIX信号量

POSIX信号量包括命名和未命名的,两种的差异在于创建和销毁的形式上,但其他工作一样。

未命名信号量只存在于内存中,且要求能使用信号量的进程必须可以访问内存,故而它们只能应用在同一进程中的线程,或者不同进程中已经映射相同内存内容到它们的地址空间中的线程。

命名信号量可以通过名字访问,因此可以被任何已知它们名字的进程中的线程使用。

命名信号量

调用sem_open函数来创建一个新的命名信号量或者使用一个现有信号量:

#include<semaphore.h>
sem_t *sem_open(const char *name,int oflag,...,mode_t mode,unsigned int value);

若成功返回指向信号量的指针,若出错返回SEM_FAILED。

当使用一个现有的命名信号量时,只需指定两个参数:信号量的名字和oflag参数的0值。当这个oflag参数有O_CREAT标志集时,若命名信号量不存在,则创建一个新的。若已经存在则会被使用但不会有额外的初始化发生。当我们指定O_CREAT标志时,需要提供两个额外的参数:mode参数指定谁可以访问信号量,value参数用来指定信号量的初始值。

若想确保创建的是信号量,可以设置oflag参数为O_CREAT|O_EXCL。若信号量已经存在,会导致sem_open失败。

信号量命名也需要遵循以下规则:

  • 名字的第一个字符应为斜杠(/)
  • 名字不应包含其他斜杠
  • 信号量名字不应该长于_POSIX_NAME_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_wait或sem_trywait或sem_timedwait来实现信号量的减1操作:

#include<semaphore.h>
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);     //非阻塞,信号量为0返回-1且errno=EAGAIN
int sem_timedwait(sem_t *sem,const struct timespec *tsptr);  //超时返回-1且error=ETIMEDOUT

若成功返回0,出错返回-1。

sem_post函数使信号量增1:

#include<semaphore.h>
int sem_post(sem_t *sem);

若成功返回0,出错返回-1。

未命名信号量

调用sem_init函数创建一个未命名的信号量:

#include<semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigned int value);

若成功返回0,出错返回-1。

pshared参数表明是否在多个进程中使用信号量,若是将其设置成一个非0值。value参数指定了信号量的初始值。需要声明一个sem_t类型的变量并把它的地址传递给sem_init来实现初始化,若要在两个进程间使用信号量,需要确保sem参数指向两个进程之间共享的内存范围。

调用sem_destroy函数丢弃它:

#include<semaphore.h>
int sem_destroy(sem_t *sem);

若成功返回0,出错返回-1。

调用sem_destroy后,不能再使用任何带有sem的信号量函数,除非通过调用sem_init重新初始化它。

sem_getvalue函数可以用来检索信号量值。

#include<semaphore.h>
int sem_getvalue(sem_t *sem,int *valp);

若成功返回0,出错返回-1。

成功后valp指向的整数值将包含信号量值。

猜你喜欢

转载自blog.csdn.net/sinat_30477313/article/details/80502526