31、POSIX信号量与文件锁的深度解析

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 系统调用可能更合适。

通过合理使用信号量和文件锁,可以确保程序在多进程和多线程环境下的正确性和性能。

"Mstar Bin Tool"是一款专门针对Mstar系列芯片开发的固件处理软件,主要用于智能电视及相关电子设备的系统维护深度定制。该工具包特别标注了"LETV USB SCRIPT"模块,表明其对乐视品牌设备具有兼容性,能够通过USB通信协议执行固件读写操作。作为一款专业的固件编辑器,它允许技术人员对Mstar芯片的底层二进制文件进行解析、修改重构,从而实现系统功能的调整、性能优化或故障修复。 工具包中的核心组件包括固件编译环境、设备通信脚本、操作界面及技术文档等。其中"letv_usb_script"是一套针对乐视设备的自动化操作程序,可指导用户完成固件烧录全过程。而"mstar_bin"模块则专门处理芯片的二进制数据文件,支持固件版本的升级、降级或个性化定制。工具采用7-Zip压缩格式封装,用户需先使用解压软件提取文件内容。 操作前需确认目标设备采用Mstar芯片架构并具备完好的USB接口。建议预先备份设备原始固件作为恢复保障。通过编辑器修改固件参数时,可调整系统配置、增删功能模块或修复已知缺陷。执行刷机操作时需严格遵循脚本指示的步骤顺序,保持设备供电稳定,避免中断导致硬件损坏。该工具适用于具备嵌入式系统知识的开发人员或高级用户,在进行设备定制化开发、系统调试或维护修复时使用。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值