POSIX 信号量

    在 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 函数中打开一个信号量后又断开了它的连接,这销毁了名字,所以其他进程不能再次访问它,同时也简化了进程结束时的清理工作。

猜你喜欢

转载自aisxyz.iteye.com/blog/2420193