zoukankan      html  css  js  c++  java
  • TLPI读书笔记第53章-POSIX信号量

    本章将介绍 POSIX 信号量,它允许进程和线程同步对共享资源的访问。在 47 章中介绍了System V 信号量,本章假设读者已经熟悉了信号量的一般概念以及本章开头部分介绍的信号量的使用原理。 在讲述本章内容的过程中将会对 POSIX 信号量和 System V 信号量进行比较以阐明这两组信号量 API 的相同之处和相异之处。

    53.1 概述

    SUSv3 规定了两种类型的 POSIX 信号量。

    1.命名信号量:这种信号量拥有一个名字。通过使用相同的名字调用 sem_open(),不相关的进程能够访问同一个信号量。

    2.未命名信号量:这种信号量没有名字,相反,它位于内存中一个预先商定的位置处。未命名信号量可以在进程之间或一组线程之间共享。当在进程之间共享时,信号量必须位于一个共享内存区域中(System V、 POSIX 或 mmap())。当在线程之间共享时,信号量可以位于被这些线程共享的一块内存区域中(如在堆上或在一个全局变量中)。

    POSIX 信号量的运作方式与 System V 信号量类似,即 POSIX 信号量是一个整数,其值是不能小于 0 的。如果一个进程试图将一个信号量的值减小到小于 0,那么取决于所使用的函数,调用会阻塞或返回一个表明当前无法执行相应操作的错误。

    一些系统并没有完整地实现 POSIX 信号量,一个典型的约束是只支持未命名线程共享的信号量。在 Linux 2.4 上也是同样的情况;只有在 Linux 2.6 以及带 NPTL 的 glibc 上,完整的POSIX 信号量实现才可用。

    53.2 命名信号量

    要使用命名信号量必须要使用下列函数

    sem_open()函数打开或创建一个信号量并返回一个句柄以供后续调用使用,如果这个调用会创建信号量的话还会对所创建的信号量进行初始化。

    sem_post(sem)和 sem_wait(sem)函数分别递增和递减一个信号量值。

    sem_getvalue()函数获取一个信号量的当前值。

    sem_close()函数删除调用进程与它之前打开的一个信号量之间的关联关系。

    sem_unlink()函数删除一个信号量名字并将其标记为在所有进程关闭该信号量时删除该信号量。

    SUSv3 并没有规定如何实现命名信号量。一些 UNIX 实现将它们创建成位于标准文件系统上一个特殊位置处的文件。在 Linux 上,命名信号量被创建成小型 POSIX 共享内存对象,其名字的形式为 sem.name,这些对象将被放在一个挂载在/dev/shm 目录之下的专用 tmpfs 文件系统中( 14.10 节)。这个文件系统具备内核持久性——它所包含的信号量对象将会持久,即使当前没有进程打开它们,但如果系统被关闭的话,这些对象就会丢失。

    在 Linux 上从内核 2.6 起开始支持命名信号量。

    53.2.1 打开一个命名信号量

    sem_open()函数创建和打开一个新的命名信号量或打开一个既有信号量。

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

    name 参数标识出了信号量,其取值需符合 51.1 节中给出的规则。 oflag 参数是一个位掩码,它确定了是打开一个既有信号量还是创建并打开一个新信号量。如果 oflag 为 0,那么将访问一个既有信号量。如果在 oflag 中指定了 O_CREAT,并且与给定的 name对应的信号量的不存在,那么就创建一个新信号量。如果在 oflag 中同时指定了 O_CREAT 和O_EXCL,并且与给定的 name 对应的信号量已经存在,那么 sem_open()就会失败。 如果 sem_open()被用来打开一个既有信号量, 那么调用就只需要两个参数。 但如果在 flags中指定了 O_CREAT,那么就还需要另外两个参数: mode 和 value。(如果与 name 对应的信号量已经存在,那么这两个参数会被忽略。 )具体如下。 1.mode 参数是一个位掩码,它指定了施加于新信号量之上的权限。这个参数能取的位值与文件上的位值是一样的(表 15-4)并且与 open()一样, mode 参数中的值会根据进程的 umask 来取掩码( 15.4.6 节)。 SUSv3 并没有为 oflag 规定任何访问模式标记(O_RDONLY、 O_WRONLY 以及 O_RDWR)。很多实现,包括 Linux,在打开一个信号量时会将访问模式默认成 O_RDWR,因为大多数使用信号量的应用程序都同时会用到sem_post()和 sem_wait(),从而需要读取和修改一个信号量的值。这意味着需要确保将读权限和写权限赋给每一类需要访问这个信号量的用户——owner、 group 以及 other。

    2.value 参数是一个无符号整数,它指定了新信号量的初始值。信号量的创建和初始化操作是原子的,这样就避免了 System V 信号量初始化时所需完成的复杂工作了(47.5 节)。

    不管是创建一个新信号量还是打开一个既有信号量, sem_open()都会返回一个指向一个 sem_t 值的指针,而在后续的调用中则可以通过这个指针来操作这个信号量。 sem_open()在发生错误时会返回 SEM_FAILED 值。(在大多数实现上, SEM_FAILED 被定义成了((sem_t *) 0)或((sem_t *) –1)); Linux 采用了前面一种定义。 SUSv3 声称当在 sem_open()的返回值指向的 sem_t 变量的副本上执行操作( sem_post()、sem_wait()等)时结果是未定义的。换句话说,像下面这种使用 sem2 的做法是不允许的。 通过 fork()创建的子进程会继承其父进程打开的所有命名信号量的引用。在 fork()之后,父进程和子进程就能够使用这些信号量来同步它们的动作了。

    53.2.2 关闭一个信号量

    当一个进程打开一个命名信号量时,系统会记录进程与信号量之间的关联关系。sem_close()函数会终止这种关联关系(即关闭信号量),释放系统为该进程关联到该信号量之上的所有资源,并递减引用该信号量的进程数。 打开的命名信号量在进程终止或进程执行了一个 exec()时会自动被关闭。 关闭一个信号量并不会删除这个信号量,而要删除信号量则需要使用 sem_unlink()。

    #include<semaphore.h>
    int sem_close(sem_t *sem);
    int sem_unlink(const char *name);
    53.2.3 删除一个命名信号量

    sem_unlink()函数删除通过 name 标识的信号量并将信号量标记成一旦所有进程都使用完这个信号量时就销毁该信号量(这可能立即发生,前提是所有打开过该信号量的进程都已经关闭了这个信号量)。

    53.3 信号量操作

    与 System V 信号量一样, 一个 POSIX 信号量也是一个整数并且系统不会允许其值小于 0。

    但 POSIX 信号量的操作不同于 System V 信号量的操作,具体包括:

    1.修改信号量值的函数———sem_post()和 sem_wait()——一次只操作一个信号量。 与之形成对比的是, System V semop()系统调用能够操作一个集合中的多个信号量。

    2.sem_post()和 sem_wait()函数只对信号量值加 1 和减 1。与之形成对比的是, semop()能够加上和减去任意一个值。

    3.System V 信号量并没有提供一个 wait-for-zero 的操作(将 sops.sem_op 字段指定为 0的 semop()调用)。

    读者看了上面的列表可能会认为, POSIX 信号量没有 System V 信号量强大,然而事实却并非如此——能够通过 System V 信号量完成的工作都可以使用 POSIX 信号量来完成。在一些情况下,使用 POSIX 信号量可能需要多做一些编程工作,但在一般应用场景中,使用 POSIX 信号量实际所需的编程量要更少。(对于大多数应用程序来讲, System V 信号量 API 过于复杂了。)

    53.3.1 等待一个信号量

    sem_wait()函数会递减(减小 1) sem 引用的信号量的值。

    #include<semaphore.h>
    int sem_wait(sem_t *sem);
    int sem_trywait(sem_t *sem);
    int sem_timewait(sem_t *sem,const struct timespec *abs_timeout);

    如果信号量的当前值大于 0,那么 sem_wait()会立即返回。如果信号量的当前值等于 0,那么 sem_wait()会阻塞直到信号量的值大于 0 为止, 当信号量值大于 0 时该信号量值就被递减并且 sem_wait()会返回。 如果一个阻塞的 sem_wait()调用被一个信号处理器中断了, 那么它就会失败并返回 EINTR错误,不管在使用 sigaction()建立这个信号处理器时是否采用了 SA_RESTART 标记。(在其他一些 UNIX 实现上, SA_RESTART 会导致 sem_wait()自动重启。) 程序清单 53-3 中的程序为 sem_wait()函数提供了一个命令行界面,稍后就会演示如何使用这个程序

    sem_trywait()函数是 sem_wait()的一个非阻塞版本。如果递减操作无法立即被执行,那么 sem_trywait()就会失败并返回 EAGAIN 错误。

    sem_timedwait()函数是 sem_wait()的另一个变体,它允许调用者为调用被阻塞的时间量指定一个限制。如果 sem_timedwait()调用因超时而无法递减信号量,那么这个调用就会失败并返回ETIMEDOUT 错误。 abs_timeout 参数是一个结构(23.4.2 节),它将超时时间表示成了自新纪元到现在为止的秒数和纳秒数的绝对值。如果需要指定一个相对超时时间,那么就必须要使用 clock_gettime()获取 CLOCK_REALTIME 时钟的当前值并在该值上加上所需的时间量来生成一个适合在sem_timedwait()中使用的 timespec 结构。

    sem_timedwait()函数最初是在 POSIX.1d (1999)中进行规定的,所有 UNIX 实现都没有提供这个函数。

    53.3.2 发布一个信号量

    sem_post()函数递增(增加 1) sem 引用的信号量的值。

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

    如果在 sem_post()调用之前信号量的值为 0,并且其他某个进程(或线程)正在因等待递减这个信号量而阻塞,那么该进程会被唤醒,它的 sem_wait()调用会继续往前执行来递减这个信号量。如果多个进程(或线程)在 sem_wait()中阻塞了,并且这些进程的调度采用的是默认的循环时间分享策略,那么哪个进程会被唤醒并允许递减这个信号量是不确定的。(与 SystemV 信号量一样, POSIX 信号量仅仅是一种同步机制,而不是一种排队机制。 ) SUSv3 规定如果进程或线程执行在实时调度策略下,那么优先级最高等待时间最长的进程或线程将会被唤醒。 与 System V 信号量一样, 递增一个 POSIX 信号量对应于释放一些共享资源以供其他进程或线程使用。 程序清单 53-4 中的程序为 sem_post()函数提供了一个命令行界面,稍后会演示如何使用这个程序。 程序清单 53-4:使用 sem_post()递增一个 POSIX 信号量

    53.3.3 获取信号量的当前值

    sem_getvalue()函数将 sem 引用的信号量的当前值通过 sval 指向的 int 变量返回

    如果一个或多个进程(或线程)当前正在阻塞以等待递减信号量值,那么 sval 中的返回值将取决于实现。 SUSv3 允许两种做法: 0 或一个绝对值等于在 sem_wait()中阻塞的等待者数目的负数。 Linux 和其他一些实现采用了第一种行为,而另一些实现则采用了后一种行为。 尽管当存在被阻塞的等待者时在 sval 中返回一个负值是有用的,特别是对于调试来讲,但 SUSv3 并没有规定这种行为, 因为一些系统用来高效地实现 POSIX 信号量的技术没有(实际上是无法)记录被阻塞的等待者的数目。

    注意在 sem_getvalue()返回时, sval 中的返回值可能已经过时了。依赖于 sem_getvalue()返回的信息在执行后续操作时未发生变化的程序将会碰到检查时、使用时( time-of-check、time-of-use)的竞争条件(38.6 节)。

    程序清单 53-5 使用了 sem_getvalue()来获取名字通过命令行参数指定的信号量的值, 然后 在标准输出上显示该值。

    后台命令将会阻塞,这是因为信号量的当前值为 0,从而无法递减这个信号量。接着获取这个信号量的值。 从上面可以看到值 0。在其他一些实现上可能会看到值-1,表示存在一个进程正在等待这个信号量。 接着执行一个命令来递增这个信号量, 这将会导致后台程序中被阻塞的 sem_wait()调用完成执行。 (上面输出中的最后一行表明 shell 提示符会与后台作业的输出混合在一起。) 按下回车后就能看到下一个shell提示符, 这也会导致shell报告已终止的后台作业的信息。接着在信号量上执行后续的操作。

    53.4 未命名信号量

    未命名信号量(也被称为基于内存的信号量)是类型为 sem_t 并存储在应用程序分配的内存中的变量。通过将这个信号量放在由几个进程或线程共性的内存区域中就能够使这个信号量对这些进程或线程可用。 操作未命名信号量所使用的函数与操作命名信号量使用的函数是一样的( sem_wait()、sem_post()以及 sem_getvalue()等)。

    此外,还需要用到另外两个函数。

    1.sem_init()函数对一个信号量进行初始化并通知系统该信号量会在进程间共享还是在单个进程中的线程间共享。

    2.sem_destroy(sem)函数销毁一个信号量。

    这些函数不应该被应用到命名信号量上。

    未命名与命名信号量对比

    使用未命名信号量之后就无需为信号量创建一个名字了,这种做法在下列情况中是比较有用的。 1.在线程间共享的信号量不需要名字。将一个未命名信号量作为一个共享(全局或堆上的)变量自动会使之对所有线程可访问。 2.在相关进程间共享的信号量不需要名字。如果一个父进程在一块共享内存区域中(如一个共享匿名映射)分配了一个未命名信号量,那么作为 fork()操作的一部分,子进程会自动继承这个映射,从而继承这个信号量。 3.如果正在构建的是一个动态数据结构(如二叉树),并且其中的每一项都需要一个关联的信号量,那么最简单的做法是在每一项中都分配一个未命名信号量。为每一项打开一个命名信号量需要为如何生成每一项中的信号量名字(唯一的)和管理这些名字设计一个规则(如当不再需要它们时就对它们进行断开链接操作)。

    53.4.1 初始化一个未命名信号量

    sem_init()函数使用 value 中指定的值来对 sem 指向的未命名信号量进行初始化。

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

    pshared 参数表明这个信号量是在线程间共享还是在进程间共享。 1.如果 pshared 等于 0,那么信号量将会在调用进程中的线程间进行共享。在这种情况下, sem 通常被指定成一个全局变量的地址或分配在堆上的一个变量的地址。线程共享的信号量具备进程持久性,它在进程终止时会被销毁。 2.如果 pshared 不等于 0,那么信号量将会在进程间共享。在这种情况下, sem 必须是共享内存区域(一个 POSIX 共享内存对象、一个使用 mmap()创建的共享映射、或一个System V 共享内存段)中的某个位置的地址。信号量的持久性与它所处的共享内存的持久性是一样的。(通过其中大部分技术创建的共享内存区域具备内核持久性。但共享匿名映射是一个例外,只要存在一个进程维持着这种映射,那么它就一直存在下去。)由于通过 fork()创建的子进程会继承其父进程的内存映射,因此进程共享的信号量会被通过 fork()创建的子进程继承,这样父进程和子进程也就能够使用这些信号量来同步它们的动作了。

    之所以需要 pshared 参数是因为下列原因。

    1.一些实现不支持进程间共享的信号量。在这些系统上为 pshared 指定一个非零值会导致 sem_init()返回一个错误。 Linux 直到内核 2.6 以及 NPTL 线程化技术的出现之后才开始支持未命名的进程间共享的信号量。

    2.在同时支持进程间共享信号量和线程间共享信号量的实现上,指定采用何种共享方式是有必要的,因为系统必须要执行特殊的动作来支持所需的共享方式。提供此类信息还使得系统能够根据共享的种类来执行优化工作。 NPTL sem_init()实现会忽略 pshared, 因为不管采用何种共享方式都无需执行特殊的动作,但可移植的以及面向未来的应用程序应该为 pshared 指定一个恰当的值未命名信号量不存在相关的权限设置(即 sem_init()中并不存在在 sem_open()中所需的mode 参数)。对一个未命名信号量的访问将由进程在底层共享内存区域上的权限来控制。

    SUSv3 规定对一个已初始化过的未命名信号量进行初始化操作将会导致未定义的行为。换句话说,必须要将应用程序设计成只有一个进程或线程来调用 sem_init()以初始化一个信号量。

    与命名信号量一样, SUSv3 声称在地址通过传入 sem_init()的 sem 参数指定的 sem_t 变量的副本上执行操作的结果是未定义的,因此应该总是只在“最初的”信号量上执行操作。

    53.4.2 销毁一个未命名信号量

    sem_destroy()函数将销毁信号量 sem,其中 sem 必须是一个之前使用 sem_init()进行初始化的未命名信号量。只有在不存在进程或线程在等待一个信号量时才能够安全销毁这个信号量。当使用 sem_destroy()销毁了一个未命名信号量之后就能够使用 sem_init()来重新初始化这个信号量了。

    一个未命名信号量应该在其底层的内存被释放之前被销毁。例如,如果信号量一个自动分配的变量, 那么在其宿主函数返回之前就应该销毁这个信号量。 如果信号量位于一个 POSIX共享内存区域中,那么在所有进程都使用完这个信号量以及在使用 shm_unlink()对这个共享内存对象执行断开链接操作之前应该销毁这个信号量。

    在一些实现上,省略 sem_destroy()调用不会导致问题的发生,但在其他实现上,不调用sem_destroy()会导致资源泄露。

    可移植的应用程序应该调用 sem_destroy()以避免此类问题的发生。

    53.5 与其他同步技术比较

    本节将比较 POSIX 信号量和其他两种同步技术: System V 信号量和互斥体

    POSIX 信号量与 System V 信号量比较

    POSIX 信号量和 System V 信号量都可以用来同步进程的动作。 51.2 节列出了 POSIX IPC与 System V IPC 相比具备的几项优势: POSIX IPC 接口更加简单并且与传统的 UNIX 文件模型更加一致,同时 POSIX IPC 对象是引用计数的,这样就简化了确定何时删除一个 IPC 对象的工作。这些常规优势同样也是 POSIX(命名)信号量优于 System V 信号量的地方。

    优势

    与 System V 信号量相比, POSIX 信号量还具备下列优势。

    1.POSIX 信号量接口与 System V 信号量接口相比要简单许多。这种简单性并没有以牺牲功能的强大性为代价。 2.POSIX 命名信号量消除了 System V 信号量存在的初始化问题(47.5 节)。 3.将一个 POSIX 未命名信号量与动态分配的内存对象关联起来更加简单:只需要将信号量嵌入到对象中即可。 4.在高度频繁地争夺信号量的场景中(即信号量上的操作经常因另一个进程将信号量值设置成一个阻止操作立即往前执行的的值而阻塞),那么 POSIX 信号量的性能与System V 信号量的性能是类似的。但在争夺信号量不那么频繁的场景中(即信号量的值能够让操作正常向前执行而不会阻塞操作), POSIX 信号量的性能要比 System V 信 号量好很多。 POSIX 在这种场景中之所以能够做得更好是因为它们的实现方式只有在发生争夺的时候才需要执行系统调用, 而 System V 信号量操作则不管是否发生争夺都需要执行系统调用。

    劣势

    然而 POSIX 信号量与 System V 信号量相比也存在下列劣势。

    1.POSIX 信号量的可移植性稍差。(在 Linux 上,直到内核 2.6 才开始支持命名信号量。) 2.POSIX 信号量不支持 System V 信号量中的撤销特性。(然而在 47.8 节中指出过这个特性在一些场景中可能并没有太大的用处。)

    POSIX 信号量与 Pthreads 互斥体对比

    POSIX 信号量和 Pthreads 互斥体都可以用来同步同一个进程中的线程的动作,并且它们的性能也是相近的。然而互斥体通常是首选方法,因为互斥体的所有权属性能够确保代码具有良好的结构性(只有锁住互斥体的线程才能够对其进行解锁)。与之形成对比的是,一个线程能够递增一个被另一个线程递减的信号量。这种灵活性会导致产生结构糟糕的同步设计。

    (正是因为这个原因,信号量有时候会被称为并发式编程中的“goto”。) 互斥体在一种情况下是不能用在多线程应用程序中的,在这种情况下信号量可能就成了一种首选方法了。由于信号量是异步信号安全的(参见表 21-1),因此在一个信号处理器中可以使用 sem_post()函数来与另一个线程进行同步。而信号量就无法完成这项工作,因为操作互斥体的 Pthreads 函数不是异步信号安全的。然而通常处理异步信号的首选方法是使用sigwaitinfo()(或类似的函数)来接收这些信号,而不是使用信号处理器(33.2.4 节),因此信号量比互斥体在这一点上的优势很少有机会发挥出来。

    53.6 信号量的限制

    SUSv3 为信号量定义了两个限制。

    SEM_NSEMS_MAX

    这是一个进程能够拥有的 POSIX 信号量的最大数目。 SUSv3 要求这个限制至少为 256。在 Linux 上, POSIX 信号量数目实际上会受限于可用的内存。

    SEM_VALUE_MAX

    这是一个 POSIX 信号量值能够取的最大值。信号量的取值可以为 0 到这个限制之间的任意一个值。 SUSv3 要求这个限制至少为 32767, Linux 实现允许这个值最大为 INT_MAX

    53.7 总结

    POSIX 信号量允许进程或线程同步它们的动作。 POSIX 信号量有两种:命名的和未命名的。命名信号量是通过一个名字标识的,它可以被所有拥有打开这个信号量的权限的进程共享。未命名信号量没有名字,但可以将它放在一块由进程或线程共享的内存区域中,使得这些进程或线程能够共享同一个信号量(如放在一个 POSIX 共享内存对象中以供进程共享,或放在一个全局变量中以供线程共享)。

    POSIX 信号量接口比 System V 信号量接口简单。信号量的分配和操作是一个一个进行的,并且等待和发布操作只会将信号量值调整 1。

    与 System V 信号量相比, POSIX 信号量具备很多优势,但它们的可移植性要稍差一点。 对于多线程应用程序中的同步来讲,互斥体一般来讲要优于信号量

  • 相关阅读:
    性能测试学习第五天-----Jmeter测试脚本&基础元件使用
    【win10主机】连接virtualbox上【32位winXP系统虚拟机】上启动的mysql
    【win10主机】访问virtualbox上【32位winXP系统虚拟机】上启动的项目
    性能测试学习第四天-----loadrunner:jdbc批量制造测试数据 & controller应用
    appium输入法踩坑解决方案-----中文乱码及输入法搜索无法点击
    性能测试学习第三天-----loadrunner接口测试&中文乱码处理
    Ext JS学习第四天 我们所熟悉的javascript(三)
    Ext JS学习第五天 我们所熟悉的javascript(四)
    ExtJS学习第一天 MessageBox
    C#使用Zxing2.0生成二维码 带简单中心LOGO
  • 原文地址:https://www.cnblogs.com/wangbin2188/p/14864271.html
Copyright © 2011-2022 走看看