基本概念
命名信号量是一种同步原语,主要用于进程间同步和通信。它们在不同的进程之间是可见的,因此可以用来控制多个进程对共享资源的访问。以下是关于命名信号量的详细介绍:
1. 基础:
-
信号量的值:一个非负整数,通常代表一个资源或资源组的可用单位数量。
-
操作:信号量支持两种基本操作:
P
(proberen,尝试) 和V
(verhogen,增加)。在 POSIX API 中,这两种操作分别通过sem_wait
和sem_post
函数来实现。
2. 操作:
-
sem_wait (或 P 操作):尝试减少信号量的值。如果信号量的值大于0,它就简单地减少并继续执行。如果信号量的值为0,调用这个操作的进程会被阻塞,直到信号量的值变为正数。
-
sem_post (或 V 操作):增加信号量的值。如果有进程在
sem_wait
上被阻塞,其中一个进程会被唤醒。
3. 命名与匿名:
-
命名信号量:可以在多个进程之间使用,因为它们有一个与之关联的名称。这些名称并不与文件系统路径直接相关,但它们的语法类似于路径,例如
"/my_semaphore"
。 -
匿名信号量:只能在一个进程内的不同线程之间使用。它们没有与之关联的名称。
4. 命名信号量的常见函数:
-
sem_open:打开一个已存在的命名信号量,或创建一个新的命名信号量。
-
sem_close:关闭信号量的引用,但不删除信号量。
-
sem_unlink:删除一个命名信号量。实际的存储资源只有在所有进程都关闭了对信号量的引用之后才会被释放。
-
sem_post 和 sem_wait:如前所述。
5. 使用场景:
命名信号量常用于两个或多个进程需要访问共享资源时。例如,两个进程可能需要访问一个共享的文件或内存区域。通过使用命名信号量,进程可以确保在给定时间只有一个进程能够访问这些资源,从而避免冲突和数据不一致。
总之,命名信号量提供了一种有效的方式来协调和同步多个进程的活动,特别是当它们需要共享资源时。
常用API
下面,我们来看一下POSIX API 中有关命名信号量的函数。
1. sem_open
sem_t *sem_open(const char *name, int oflag, ... /* mode_t mode, unsigned int value */ );
-
目的:创建一个新的命名信号量或打开一个已存在的信号量。
-
参数:
name
:信号量的名称,通常带有一个前斜杠(例如,“/my_semaphore”)。oflag
:控制操作的标志。主要标志包括:O_CREAT
:如果信号量不存在,则创建它。O_EXCL
:与O_CREAT
一起使用,确保此调用创建信号量。如果信号量已经存在,调用将失败。
mode
:指定信号量的权限(类似于文件权限)。如果指定了O_CREAT
,则考虑此项。value
:信号量的初始值。同样,只有在指定了O_CREAT
时才考虑此项。
-
返回值:
- 成功:返回指向信号量对象的指针。
- 失败:返回
SEM_FAILED
,并设置errno
来指示错误。
2. sem_wait
int sem_wait(sem_t *sem);
-
目的:递减(锁定)信号量。如果信号量的值为零,调用进程将阻塞,直到它变得大于零。
-
参数:
sem
:指向信号量对象的指针。
-
返回值:
- 成功:返回0。
- 失败:返回-1,并设置
errno
来指示错误。
3. sem_post
int sem_post(sem_t *sem);
-
目的:递增(解锁)信号量。如果有进程正在等待信号量,其中一个将被解除阻塞。
-
参数:
sem
:指向信号量对象的指针。
-
返回值:
- 成功:返回0。
- 失败:返回-1,并设置
errno
来指示错误。
4. sem_close
int sem_close(sem_t *sem);
-
目的:关闭信号量。关闭的信号量除非用
sem_open
重新打开,否则不应再使用。 -
参数:
sem
:指向信号量对象的指针。
-
返回值:
- 成功:返回0。
- 失败:返回-1,并设置
errno
来指示错误。
5. sem_unlink
int sem_unlink(const char *name);
-
目的:删除一个命名信号量。在所有进程都关闭了信号量之前,可能实际上并没有被移除。
-
参数:
name
:信号量的名称。
-
返回值:
- 成功:返回0。
- 失败:返回-1,并设置
errno
来指示错误。
总的来说,信号量是同步原语,当多个进程需要访问共享资源时非常有用。命名信号量(如上述函数所操作的)可以在不相关的进程之间共享,而未命名的信号量通常用于同一进程内的线程同步。
示例
接下来,给出一个简单的例子,演示如何使用命名信号量进行进程间同步。
此例中,我们将创建两个进程:一个进程写入数据,另一个进程读取数据。
1. 写入数据的进程 (writer.c)
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <unistd.h>
#define SEM_NAME "/my_semaphore"
#define FILENAME "data.txt"
int main() {
sem_t *sem = sem_open(SEM_NAME, O_CREAT, 0666, 0);
if (sem == SEM_FAILED) {
perror("sem_open");
return 1;
}
FILE *file = fopen(FILENAME, "w");
if (!file) {
perror("fopen");
sem_unlink(SEM_NAME);
return 1;
}
fprintf(file, "Hello from writer process!\n");
fclose(file);
sem_post(sem);
sem_close(sem);
return 0;
}
2. 读取数据的进程 (reader.c)
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <unistd.h>
#define SEM_NAME "/my_semaphore"
#define FILENAME "data.txt"
int main() {
sem_t *sem = sem_open(SEM_NAME, O_RDWR);
if (sem == SEM_FAILED) {
perror("sem_open");
return 1;
}
sem_wait(sem);
FILE *file = fopen(FILENAME, "r");
if (!file) {
perror("fopen");
sem_close(sem);
return 1;
}
char buffer[256];
fgets(buffer, sizeof(buffer), file);
printf("Data from file: %s", buffer);
fclose(file);
sem_close(sem);
sem_unlink(SEM_NAME);
return 0;
}
使用说明:
-
先编译这两个文件:
gcc writer.c -o writer gcc reader.c -o reader
-
首先运行
writer
进程,它会写入数据并通过信号量通知reader
进程。 -
然后运行
reader
进程,它会等待数据。 -
reader
进程将读取文件并显示内容。
运行结果如下:
majn@tiger:~$ ./writer
majn@tiger:~$ cd /dev/shm/
majn@tiger:/dev/shm$ ls
sem.my_semaphore
majn@tiger:~$ ./reader
Data from file: Hello from writer process!
使用命名信号量,这样两个不同的进程就可以进行同步。当 writer
进程写入数据并发布信号量后,reader
进程就会被解除阻塞,然后它会读取并显示数据。
几个问题:
-
命名信号量的路径:当我们为命名信号量提供一个名称,如
"/my_semaphore"
,它并不是在文件系统的根目录下创建的。这个“路径”只是为命名信号量提供了一个独特的标识符,用于进程间的通信和同步。在很多系统上,这些信号量实际上在内核内存中被管理,而不是作为文件系统上的真实文件存在。 -
信号量的生命周期:在
writer.c
的main
函数执行结束后,使用sem_close
关闭了信号量的描述符,但这并不意味着信号量被销毁。为了完全删除信号量,需要调用sem_unlink
。如果只是调用
sem_close
,那么信号量的名字仍然存在,其他进程仍然可以通过调用sem_open
与之进行通信。实际的信号量资源(例如,与信号量相关的内核数据结构)只有在所有进程都关闭了该信号量并且它被sem_unlink
之后才会被释放。 -
sem_post(sem);
的行为:sem_post
函数用于增加信号量的值。如果有其他进程或线程正在等待这个信号量(即,它们调用了sem_wait
并被阻塞),那么sem_post
可能会导致其中一个被解阻塞。在我们的代码中,sem_post
仅仅是信号给等待的进程(可能是reader.c
中的进程)知道可以继续执行。sem_post
自身并不会等待或阻塞。
总之,为了完全删除命名信号量,需要使用 sem_unlink
。在上面的例子中,这是在 reader.c
中完成的,确保了在读取操作完成后信号量被删除。