欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

POSIX 信号量

程序员文章站 2022-07-01 09:14:49
...
    在XSI IPC通信之信号量一节中提到了 XSI 标准的信号量。POSIX 信号量意在解决 XSI 信号量的以下几个缺陷。
    1)POSIX 信号量考虑到了更高性能的实现。
    2)POSIX 信号量接口使用更简单:没有信号量集。
    3)POSIX 信号量在删除时表现更完美。当一个 XSI 信号量被删除时,使用这个信号量标识符的操作会失败,并将 errno 设置成 EIDRM。而使用 POSIX 信号量时,操作能继续正常工作,直到该信号量的最后一次引用被释放。
    POSIX 信号量有命名的和未命名的两种。它们的差异在于创建和销毁的形式上。命名信号量可以被任何已知它们名字的进程中的线程使用。而未命名信号量只存在于内存中,这意味着它们只能应用在同一进程中的线程,或者不同进程中已经映射相同内存内容到它们的地址空间中的线程。
    可以使用 sem_open 函数来创建一个新的命名信号量或者使用一个现有的信号量。该函数返回的信号量指针用来传递给其他信号量函数。当完成信号量操作时,可以调用 sem_close 函数来释放任何与信号量相关的资源。sme_unlink 函数则可以用来销毁一个命名信号量。
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag, ... /* mode_t mode, unsigned int value */);
              /* 返回值:若成功,返回指向信号量的指针;若出错,返回 SEM_FAILED */
int sem_close(sem_t *sem);
int sem_unlink(const char *name);
                           /* 两个函数的返回值:若成功,返回 0;否则,返回 -1 */

    sem_open 函数中,当使用一个现有的命名信号量时,只需要指定前两个参数:信号量的名字 name 和 oflag 的 0 值。当 oflag 参数有 O_CREAT 标志时,如果命名信号量不存在,则会创建。如果已经存在,则会被使用,但不会有额外的初始化发生。在指定 O_CREAT 标志时,需要提供后两个额外的参数。其中 mode 参数指定信号量的使用权限,其取值同文件的权限位。赋值给信号量的权限可以被调用者的文件创建屏蔽字修改。另一个参数 value 则指定信号量的初始值,它的取值范围是:0 ~ SEM_VALUE_MAX(见unix限制一节)。如果想确保创建的是信号量,可以设置 oflag 参数为 O_CREAT|O_EXCL。这样如果信号量已经存在,会导致 sem_open 函数失败,并将 errno 置为 EEXIST。
    为了增加可移植性,命名信号量时必须遵循以下规则。
    1)名字的第一个字符应该为“/”,以便在 POSIX 信号量的实现使用了文件系统时消除名字的二义性。
    2)名字不应包含多余斜杠以避免实现定义的行为。比如,如果使用了文件系统,那么 /mysem 和 //mysem 会被认为是同一个文件名,但如果没使用,则它们可以被认为不同。
    3)信号量名字的最大长度是实现定义的,不应该长于 _POSIX_NAME_MAX 个字符,因为这是使用文件系统的实现能允许的最大名字长度限制。
    如果进程没有调用 sem_close 函数而退出,那么内核将自动关闭任何打开的信号量。注意,这不会影响信号量值的状态——如果已经对它进行了增 1 操作,这不会因为退出而改变。类似地,信号量值也不会因为调用了 sem_close 函数而受到影响。
    sem_unlink 函数会删除信号量的名字。如果没有打开的信号量引用,则立即销毁。否则,销毁将延迟到最后一个打开的引用关闭。

    当想要在单个进程中使用 POSIX 信号量时,使用未命名信号量会更容易。相对于命名信号量,这仅仅需要改变创建和销毁信号量的方式。
    可以调用 sem_init 函数来创建一个未命名的信号量。对未命名信号量的使用完成时,可以调用 sem_destroy 函数来丢弃它。
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
                           /* 两个函数的返回值:若成功,返回 0;否则,返回 -1 */

    sem_init 函数中的 pshared 参数为非 0 值时表示可以在多个进程中使用该信号量。value 参数指定了信号量的初始值。sem 参数代表匿名信号量的地址。如果要在多个进程之间使用信号量,需要确保该参数在它们共享的内存范围内。
    调用 sme_destroy 函数后,不能再使用任何带有 sem 的信号量函数,除非调用 sem_init 函数重新初始化它。

    信号量创建好后,就可以利用下面这些函数来操作了。其中,sem_wait 或者 sem_trywait 函数可以用来实现信号量的减 1 操作。sem_timedwait 函数则可以选择阻塞一段确定的时间。sem_post 函数则可用来使信号量值增 1。sem_getvalue 函数可以用来检索信号量值(Mac OS X 10.6.8 不支持该函数)。
#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_rywait(sem_t *sem);
int sem_timedwait(sem_t *restrict sem, const struct timespec *restrict tsptr);
int sem_post(sem_t *sem);
int sem_getvalue(sem_t *restrict sem, int *restrict valp);
                           /* 几个函数的返回值:若成功,返回 0;否则,返回 -1 */

    使用 sem_wait 函数时,如果信号量计数是 0 就会发生阻塞,直到成功使信号量减 1 或者被信号中断时才返回。使用 sem_trywait 可以避免阻塞:如果信号量是 0,则不会阻塞,而是立即返回 -1 并将 errno 置为 EAGAIN。sem_timedwait 函数中的 tsptr 参数可以指定绝对时间(基于 CLOCK_REALTIME 时钟)。如果超时到期并且信号量计数没能减 1,该函数将返回 -1,并将 errno 置为 ETIMEDOUT。
    调用 sem_post 可以唤醒因调用 sem_wait 等函数而阻塞的其中一个进程,并且被 sem_post 增 1 的信号量计数会再次被 sem_wait 等函数减 1。
    sem_getvalue 函数调用成功后,valp 参数就会包含信号量值。不过要注意,该值在读出来后信号量的值可能已经变了。除非使用额外的同步机制来避免这种竞争,否则该函数只能用于调试。
    下面这段代码使用 POSIX 信号量来实现了一种锁,该锁能被一个线程加锁而被另一个线程解锁。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

struct slock{
	sem_t	*semp;
	char	name[_POSIX_NAME_MAX];
};

struct slock* s_alloc(){
	struct slock	*sp;
	static int		cnt;
	if((sp=malloc(sizeof(struct slock))) == NULL)
		return NULL;
	do{
		snprintf(sp->name, sizeof(sp->name), "/%ld.%d", (long)getpid(), cnt++);
		sp->semp = sem_open(sp->name, O_CREAT|O_EXCL, S_IRWXU, 1);
	}while(sp->semp == SEM_FAILED && errno == EEXIST);
	if(sp->semp == SEM_FAILED){
		free(sp);
		return NULL;
	}
	sem_unlink(sp->name);
	return sp;
}

void s_free(struct slock *sp){
	sem_close(sp->semp);
	free(sp);
}

int s_lock(struct slock *sp){
	return sem_wait(sp->semp);
}

int s_trylock(struct slock *sp){
	return sem_trywait(sp->semp);
}

int s_unlock(struct slock *sp){
	return sem_post(sp->semp);
}

    这里根据进程 ID 和计数器来创建名字。注意,这里没必要用互斥量来保护计数器,因为当两个竞争的线程同时调用 s_alloc 并以同一个名字结束时,在调用 sem_open 中使用 O_EXCL 标志将会使其中一个成功而另一个失败,失败的线程会将 errno 设置成 EEXIST,然后会再次尝试。另外,在 s_alloc 函数中打开一个信号量后又断开了它的连接,这销毁了名字,所以其他进程不能再次访问它,同时也简化了进程结束时的清理工作。