POSIX信号量与文件锁的深度解析
1. POSIX信号量概述
POSIX信号量是POSIX标准的一部分,不过是可选的。在使用前,需要检查
_POSIX_SEMAPHORES
特性测试宏,以确定系统是否支持。目前,FreeBSD、Darwin或Linux系统中可能未包含这些信号量。
1.1 System V信号量的额外用途
System V信号量除了常规用途外,还能在进程间传递整数。例如,客户端可以获取信号量并将其值设置为自身的进程ID,服务器随后读取该值就能得到相应信息。由于信号量集可以包含多个信号量,因此还能传递整数数组。
以下是相关代码示例:
bool SimpleSemWait(struct SimpleSem *sem)
{
struct sembuf sop;
sop.sem_num = 0;
sop.sem_op = -1;
sop.sem_flg = 0;
ec_neg1( semop(sem->sm.sm_semid, &sop, 1) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
bool SimpleSemPost(struct SimpleSem *sem)
{
struct sembuf sop;
sop.sem_num = 0;
sop.sem_op = 1;
sop.sem_flg = 0;
ec_neg1( semop(sem->sm.sm_semid, &sop, 1) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
1.2 命名POSIX信号量
命名POSIX信号量比System V信号量更简单易用。有五个系统调用与之前的
SimpleSem
接口对应:
-
sem_open
:打开命名信号量。
-
sem_close
:关闭命名信号量。
-
sem_unlink
:移除命名信号量。
-
sem_wait
:减少信号量的值。
-
sem_post
:增加信号量的值。
以下是这些系统调用的代码示例:
// sem_open
sem_t *sem_open(
const char *name,
int flags
);
sem_t *sem_open(
const char *name,
int flags,
mode_t perms,
unsigned value
);
// sem_close
int sem_close(
sem_t *sem
);
// sem_unlink
int sem_unlink(
const char *name
);
// sem_wait
int sem_wait(
sem_t *sem
);
// sem_post
int sem_post(
sem_t *sem
);
POSIX信号量是计数信号量,每次增减步长为1。每个
sem_t
对象仅代表一个信号量,而非像System V那样代表一组信号量。
在使用
sem_open
时,不需要使用
O_RDONLY
、
O_WRONLY
或
O_RDWR
,因为信号量必须同时支持
sem_post
和
sem_wait
操作才有意义。同时,要注意
sem_open
失败时返回
SEM_FAILED
,而非
NULL
。
以下是
SimpleSem
调用的POSIX信号量实现:
struct SimpleSem *SimpleSemOpen(const char *name)
{
struct SimpleSem *sem = NULL;
ec_null( sem = malloc(sizeof(struct SimpleSem)) )
if ((sem->sm.sm_sem = sem_open(name, O_CREAT, PERM_FILE, 0)) ==
SEM_FAILED)
EC_FAIL
return sem;
EC_CLEANUP_BGN
free(sem);
return NULL;
EC_CLEANUP_END
}
bool SimpleSemClose(struct SimpleSem *sem)
{
ec_neg1( sem_close(sem->sm.sm_sem) )
free(sem);
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
bool SimpleSemRemove(const char *name)
{
if (Sem_unlink(name) == -1 && errno != ENOENT)
EC_FAIL
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
bool SimpleSemPost(struct SimpleSem *sem)
{
ec_neg1( sem_post(sem->sm.sm_sem) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
bool SimpleSemWait(struct SimpleSem *sem)
{
ec_neg1( sem_wait(sem->sm.sm_sem) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
POSIX信号量还有一些额外特性:
-
sem_getvalue
:查询信号量的值,不修改也不等待。
-
sem_trywait
:非阻塞地减少信号量的值。
-
sem_timedwait
:在指定时间内尝试减少信号量的值,超时则返回。
以下是这些特性的代码示例:
// sem_getvalue
int sem_getvalue(
sem_t *restrict sem,
int *valuep
);
// sem_trywait
int sem_trywait(
sem_t *sem
);
// sem_timedwait
int sem_timedwait(
sem_t *restrict sem,
const struct timespec *time
);
1.3 未命名POSIX信号量
未命名POSIX信号量可以直接声明或动态分配
sem_t
对象。但如果自行分配,需要调用
sem_init
进行初始化,并在使用完后调用
sem_destroy
销毁。
// sem_init
int sem_init(
sem_t *sem,
int pshared,
unsigned value
);
// sem_destroy
int sem_destroy(
sem_t *sem
);
未命名信号量有以下优点:
- 适用于线程间同步,提供计数信号量功能。
- 如果位于共享内存中,可用于进程间同步,此时
sem_init
的
pshared
参数需非零。
需要注意的是,未命名信号量只能使用传递给
sem_init
的实际内存,不能使用其副本。
2. System V与POSIX信号量对比
| 信号量类型 | 优点 | 缺点 |
|---|---|---|
| System V | 普遍可用,将复杂初始化隐藏在合理接口后使用较方便 | 使用困难,特性复杂 |
| POSIX | 简单易用 | 并非所有系统都支持 |
对于进程间通信,如果注重可移植性,建议使用System V信号量;如果可移植性不重要且目标系统支持POSIX信号量,则应优先选择POSIX信号量。对于线程间通信,System V信号量过于繁琐,未命名的非进程共享POSIX信号量通常是更好的选择。
3. 进程共享互斥锁和读写锁
互斥锁可用于线程同步,若设置
PTHREAD_PROCESS_SHARED
属性,可实现进程间共享。不过,该特性并非所有支持POSIX线程的系统都实现。
读写锁与互斥锁类似,但区分读操作和写操作,可提高吞吐量。同样,也可设置
PTHREAD_PROCESS_SHARED
属性实现进程间共享。
4. 文件锁
4.1 一个糟糕的示例
为了说明文件锁的重要性,构建了一个示例应用程序。该程序有一个进程构建文件,另一个进程读取文件。文件是一个记录链表,每个记录包含一个整数数据和指向下一个记录的偏移量。
以下是相关代码:
struct rec {
int r_data;
off_t r_next;
};
int main(void)
{
pid_t pid;
ec_neg1( pid = fork() )
if (pid == 0)
process1();
else {
process2();
ec_neg1( waitpid(pid, NULL, 0) )
}
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
static void process1(void)
{
int dbfd, data;
struct rec r;
ec_neg1( dbfd = open(DBNAME, O_CREAT | O_TRUNC | O_RDWR, PERM_FILE) )
memset(&r, 0, sizeof(r));
ec_false( writerec(dbfd, &r, 0) )
for (data = 100; data >= 0; data--)
ec_false( store(dbfd, data) )
ec_neg1( close(dbfd) )
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
bool readrec(int dbfd, struct rec *r, off_t off)
{
ssize_t nread;
if ((nread = pread(dbfd, r, sizeof(struct rec), off)) ==
sizeof(struct rec))
return true;
if (nread != -1)
errno = EIO;
EC_FAIL
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
bool writerec(int dbfd, struct rec *r, off_t off)
{
ssize_t nwrote;
if ((nwrote = pwrite(dbfd, r, sizeof(struct rec), off)) ==
sizeof(struct rec))
return true;
if (nwrote != -1)
errno = EIO;
EC_FAIL
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
bool store(int dbfd, int data)
{
struct rec r, rnew;
off_t end, prev;
ec_neg1( end = lseek(dbfd, 0, SEEK_END) )
prev = 0;
ec_false( readrec(dbfd, &r, prev) )
while (r.r_next != 0) {
ec_false( readrec(dbfd, &r, r.r_next) )
if (r.r_data > data)
break;
prev = r.r_next;
}
ec_false( readrec(dbfd, &r, prev) )
rnew.r_next = r.r_next;
r.r_next = end;
ec_false( writerec(dbfd, &r, prev) )
rnew.r_data = data;
usleep(1); /* give up CPU */
ec_false( writerec(dbfd, &rnew, end) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
static void process2(void)
{
int try, dbfd;
struct rec r1, r2;
for (try = 0; try < 10; try++)
if ((dbfd = open(DBNAME, O_RDWR)) == -1) {
if (errno == ENOENT) {
continue;
}
else
EC_FAIL
}
ec_neg1( dbfd )
for (try = 0; try < 100; try++) {
ec_false( readrec(dbfd, &r1, 0) )
while (r1.r_next != 0) {
ec_false( readrec(dbfd, &r2, r1.r_next) )
if (r1.r_data > r2.r_data) {
printf("Found sorting error (try %d)\n", try);
break;
}
r1 = r2;
}
}
ec_neg1( close(dbfd) )
return;
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
运行该程序时,会出现错误,原因是两个进程同时访问同一文件,导致数据不一致。这表明需要对文件访问进行协调。
4.2 使用信号量作为文件锁
为解决上述问题,可以使用信号量防止
process2
看到不一致的文件。但使用信号量作为文件锁存在一些问题,如难以确定信号量名称、处理临时文件时不方便、管理信号量繁琐等。
以下是使用信号量作为文件锁的关键代码:
static struct SimpleSem *sem;
ec_null( sem = SimpleSemOpen("sem") )
ec_false( SimpleSemPost(sem) )
// store函数中的关键部分
ec_false( SimpleSemWait(sem) )
ec_false( readrec(dbfd, &r, prev) )
rnew.r_next = r.r_next;
r.r_next = end;
ec_false( writerec(dbfd, &r, prev) )
rnew.r_data = data;
usleep(1); /* give up CPU */
ec_false( writerec(dbfd, &rnew, end) )
ec_false( SimpleSemPost(sem) )
// process2函数中的关键部分
for (try = 0; try < 100; try++) {
ec_false( SimpleSemWait(sem) )
ec_false( readrec(dbfd, &r1, 0) )
while (r1.r_next != 0) {
ec_false( readrec(dbfd, &r2, r1.r_next) )
if (r1.r_data > r2.r_data) {
printf("Found sorting error (try %d)\n", try);
break;
}
r1 = r2;
}
ec_false( SimpleSemPost(sem) )
}
4.3 lockf系统调用
UNIX提供了
lockf
系统调用用于锁定文件的某个部分。
// lockf
int lockf(
int fd,
int op,
off_t len
);
该系统调用的文件描述符必须以写模式打开。锁定或解锁的部分从当前文件偏移量开始,长度由
len
指定。如果
len
为零,则锁定从当前偏移量到文件末尾的部分。
op
参数有以下操作:
-
F_LOCK
:锁定部分,若部分已被其他进程锁定则阻塞。
-
F_TLOCK
:类似
F_LOCK
,但若会阻塞则返回 -1。
-
F_TEST
:不锁定,若
F_LOCK
会阻塞则返回错误。
-
F_ULOCK
:解锁部分。
以下是在示例中使用
lockf
的关键代码:
// store函数中的关键部分
ec_neg1( lseek(dbfd, 0, SEEK_SET) )
ec_neg1( lockf(dbfd, F_LOCK, 0) )
ec_false( readrec(dbfd, &r, prev) )
rnew.r_next = r.r_next;
r.r_next = end;
ec_false( writerec(dbfd, &r, prev) )
rnew.r_data = data;
usleep(1); /* give up CPU */
ec_false( writerec(dbfd, &rnew, end) )
ec_neg1( lseek(dbfd, 0, SEEK_SET) )
ec_neg1( lockf(dbfd, F_ULOCK, 0) )
// process2函数中的关键部分
for (try = 0; try < 100; try++) {
ec_neg1( lseek(dbfd, 0, SEEK_SET) )
ec_neg1( lockf(dbfd, F_LOCK, 0) )
ec_false( readrec(dbfd, &r1, 0) )
while (r1.r_next != 0) {
ec_false( readrec(dbfd, &r2, r1.r_next) )
if (r1.r_data > r2.r_data) {
printf("Found sorting error (try %d)\n", try);
break;
}
r1 = r2;
}
ec_neg1( lseek(dbfd, 0, SEEK_SET) )
ec_neg1( lockf(dbfd, F_ULOCK, 0) )
}
4.4 fcntl系统调用用于文件锁定
fcntl
系统调用的锁定功能是
lockf
的超集。
// fcntl
int fcntl(
int fd,
int op,
...
);
// struct flock
struct flock {
short l_type;
short l_whence;
off_t l_start;
off_t l_len;
pid_t l_pid;
};
fcntl
支持读锁(共享锁)和写锁(排他锁)。有三个操作与文件锁定相关:
-
F_SETLK
:尝试执行指定操作,若无法立即锁定则返回 -1。
-
F_SETLKW
:类似
F_SETLK
,但会阻塞直到锁定成功。
-
F_GETLK
:返回第一个会导致指定锁定操作阻塞的锁的信息。
fcntl
能完成
lockf
的所有功能,还能区分共享锁和排他锁,并可获取现有锁的信息。
4.5 建议性锁和强制性锁
-
建议性锁:
lockf和fcntl设置的锁通常只影响调用这些函数的操作,其他进程不调用这些函数仍可进行I/O操作。 - 强制性锁:设置锁后,会真正禁止冲突的I/O操作。POSIX和SUS标准未明确规定强制性锁,但也不禁止。在支持强制性锁的系统中,需通过设置文件权限(设置组ID执行位,关闭组执行位)来启用。
以下是一个根据参数使用建议性或强制性锁的示例程序:
int main(int argc, char *argv[])
{
int fd;
mode_t perms = PERM_FILE;
if (fork() == 0) {
sleep(1); /* wait for parent */
ec_neg1( fd = open("tmpfile", O_WRONLY | O_NONBLOCK) )
ec_neg1( write(fd, "x", 1) )
printf("child wrote OK\n");
}
else {
(void)unlink("tmpfile");
if (argc == 2)
perms |= S_ISGID; /* mandatory locking */
ec_neg1( fd = open("tmpfile", O_CREAT | O_RDWR, perms) )
ec_neg1( lockf(fd, F_LOCK, 0) )
printf("parent has lock\n");
ec_neg1( wait(NULL) )
}
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
综上所述,在进行进程间通信和文件操作时,需要根据具体需求选择合适的信号量和文件锁机制,以确保数据的一致性和程序的正确性。
POSIX信号量与文件锁的深度解析
5. 信号量和文件锁的实际应用场景分析
在实际开发中,信号量和文件锁的应用场景十分广泛,下面我们来详细分析一些常见的场景。
5.1 多线程任务同步
在多线程编程中,经常会遇到多个线程需要访问共享资源的情况。例如,一个线程负责生产数据,另一个线程负责消费数据,这就需要使用信号量来进行同步。
graph TD;
A[生产者线程] -->|生产数据| B(信号量S1);
B -->|信号量S1+1| C[消费者线程];
C -->|消费数据| D(信号量S2);
D -->|信号量S2+1| A;
在这个流程图中,生产者线程生产数据后,信号量S1的值加1,消费者线程检测到S1的值大于0时,开始消费数据,消费完成后信号量S2的值加1,生产者线程检测到S2的值大于0时,继续生产数据。这样就实现了生产者和消费者线程之间的同步。
以下是一个简单的示例代码:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
sem_t sem_producer;
sem_t sem_consumer;
int data = 0;
void *producer(void *arg) {
for (int i = 0; i < 5; i++) {
sem_wait(&sem_consumer);
data = i;
printf("Producer produced: %d\n", data);
sem_post(&sem_producer);
}
return NULL;
}
void *consumer(void *arg) {
for (int i = 0; i < 5; i++) {
sem_wait(&sem_producer);
printf("Consumer consumed: %d\n", data);
sem_post(&sem_consumer);
}
return NULL;
}
int main() {
pthread_t producer_thread, consumer_thread;
sem_init(&sem_producer, 0, 0);
sem_init(&sem_consumer, 0, 1);
pthread_create(&producer_thread, NULL, producer, NULL);
pthread_create(&consumer_thread, NULL, consumer, NULL);
pthread_join(producer_thread, NULL);
pthread_join(consumer_thread, NULL);
sem_destroy(&sem_producer);
sem_destroy(&sem_consumer);
return 0;
}
5.2 多进程文件操作
在多进程环境中,多个进程可能会同时访问同一个文件,这就需要使用文件锁来保证数据的一致性。例如,一个进程负责写入文件,另一个进程负责读取文件,使用文件锁可以避免读取到不一致的数据。
graph TD;
A[写入进程] -->|请求写锁| B(文件);
B -->|授予写锁| A;
A -->|写入数据| B;
A -->|释放写锁| B;
C[读取进程] -->|请求读锁| B;
B -->|授予读锁| C;
C -->|读取数据| B;
C -->|释放读锁| B;
以下是一个使用
fcntl
实现文件读写锁的示例代码:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
void *writer(void *arg) {
int fd = *(int *)arg;
struct flock lock;
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
fcntl(fd, F_SETLKW, &lock);
printf("Writer got the lock and is writing...\n");
write(fd, "Hello, World!", 13);
printf("Writer finished writing and releasing the lock...\n");
lock.l_type = F_UNLCK;
fcntl(fd, F_SETLK, &lock);
return NULL;
}
void *reader(void *arg) {
int fd = *(int *)arg;
struct flock lock;
lock.l_type = F_RDLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
fcntl(fd, F_SETLKW, &lock);
printf("Reader got the lock and is reading...\n");
char buffer[100];
lseek(fd, 0, SEEK_SET);
read(fd, buffer, 13);
buffer[13] = '\0';
printf("Reader read: %s\n", buffer);
printf("Reader finished reading and releasing the lock...\n");
lock.l_type = F_UNLCK;
fcntl(fd, F_SETLK, &lock);
return NULL;
}
int main() {
int fd = open("test.txt", O_CREAT | O_RDWR, 0666);
if (fd == -1) {
perror("open");
return 1;
}
pthread_t writer_thread, reader_thread;
pthread_create(&writer_thread, NULL, writer, &fd);
pthread_create(&reader_thread, NULL, reader, &fd);
pthread_join(writer_thread, NULL);
pthread_join(reader_thread, NULL);
close(fd);
return 0;
}
6. 信号量和文件锁的性能考虑
在使用信号量和文件锁时,需要考虑它们的性能问题。
6.1 信号量性能
- 命名信号量:由于需要在文件系统中创建和管理信号量对象,命名信号量的创建和销毁操作相对较慢。
- 未命名信号量:未命名信号量直接在内存中创建和管理,创建和销毁操作相对较快。
因此,在对性能要求较高的场景中,建议使用未命名信号量。
6.2 文件锁性能
- 建议性锁:建议性锁只对调用了锁函数的进程有效,其他进程可以绕过锁进行I/O操作,因此性能相对较高。
- 强制性锁:强制性锁会真正禁止冲突的I/O操作,需要内核进行更多的检查和管理,性能相对较低。
在实际应用中,应根据具体情况选择合适的锁类型。
7. 总结
本文详细介绍了POSIX信号量和文件锁的相关知识,包括信号量的类型(命名和未命名)、文件锁的系统调用(
lockf
和
fcntl
)、建议性锁和强制性锁的区别等。同时,通过实际应用场景和示例代码,展示了如何使用信号量和文件锁来实现进程间通信和文件操作的同步。
在选择信号量和文件锁时,需要考虑以下因素:
- 可移植性:如果需要在不同的系统上运行程序,应优先选择System V信号量;如果目标系统支持POSIX信号量且可移植性不是关键因素,则可以选择POSIX信号量。
- 性能:根据具体的应用场景,选择性能较高的信号量和文件锁类型。
- 功能需求:如果需要区分读锁和写锁,应选择
fcntl
系统调用;如果只需要简单的文件锁定,
lockf
系统调用可能更合适。
通过合理使用信号量和文件锁,可以确保程序在多进程和多线程环境下的正确性和性能。
超级会员免费看
1102

被折叠的 条评论
为什么被折叠?



