MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言信号量操作函数详解

2024-02-015.1k 阅读

信号量基本概念

在Linux系统编程中,信号量(Semaphore)是一种用于进程间或线程间同步的机制。它本质上是一个计数器,通过控制这个计数器的值来实现对共享资源访问的控制。信号量的值表示当前可用的共享资源数量,当一个进程或线程想要访问共享资源时,它需要先获取信号量(即减少信号量的值),如果信号量的值为0,表示没有可用资源,该进程或线程就需要等待。当进程或线程使用完共享资源后,会释放信号量(即增加信号量的值),以便其他进程或线程可以获取。

信号量有两种类型:二进制信号量和计数信号量。二进制信号量的值只能是0或1,通常用于实现互斥锁,保证同一时间只有一个进程或线程可以访问共享资源。计数信号量的值可以是任意非负整数,用于控制对多个共享资源实例的访问。

信号量相关函数概述

在Linux C语言编程中,主要通过以下几个函数来操作信号量:

  1. semget():用于创建一个新的信号量集或获取一个已存在的信号量集。
  2. semop():用于对信号量集中的信号量进行操作,如获取或释放信号量。
  3. semctl():用于对信号量集进行控制操作,如初始化信号量的值、删除信号量集等。

semget函数详解

函数原型

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);

参数说明

  1. key:是一个key_t类型的键值,它用于唯一标识一个信号量集。可以通过ftok()函数生成一个有效的key值。如果keyIPC_PRIVATE,则会创建一个私有的信号量集,只有创建该信号量集的进程及其子进程可以访问。
  2. nsems:指定要创建或获取的信号量集中信号量的数量。如果是获取已存在的信号量集,这个值必须与创建时的值相同。
  3. semflg:是一组标志位,用于指定信号量集的创建和访问权限。常见的标志位有IPC_CREAT(如果信号量集不存在则创建)、IPC_EXCL(与IPC_CREAT一起使用,确保信号量集是新创建的,如果已存在则返回错误),以及文件权限位(如0666表示读写权限)。

返回值

成功时,semget()返回一个非负整数,即信号量集的标识符(semid),后续操作可以通过这个标识符来访问信号量集。失败时,返回-1,并设置errno来指示错误原因。例如,EEXIST表示信号量集已存在且使用了IPC_CREAT | IPC_EXCL标志,ENOENT表示信号量集不存在且未使用IPC_CREAT标志。

示例代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>

#define SEM_KEY 1234
#define SEM_NUM 1

int main() {
    int semid;
    // 创建一个信号量集,包含1个信号量
    semid = semget(SEM_KEY, SEM_NUM, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget");
        exit(1);
    }
    printf("Semaphore set created successfully. Semid: %d\n", semid);
    // 这里可以进行后续对信号量集的操作
    // 最后记得释放信号量集
    if (semctl(semid, 0, IPC_RMID) == -1) {
        perror("semctl IPC_RMID");
        exit(1);
    }
    return 0;
}

在上述代码中,通过semget()函数创建了一个包含1个信号量的信号量集,使用SEM_KEY作为键值。如果创建成功,打印出信号量集的标识符。最后通过semctl()函数删除了该信号量集。

semop函数详解

函数原型

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, unsigned nsops);

参数说明

  1. semid:是由semget()函数返回的信号量集的标识符。
  2. sops:是一个指向struct sembuf类型数组的指针,struct sembuf结构体定义如下:
struct sembuf {
    unsigned short sem_num;  /* semaphore number */
    short sem_op;           /* semaphore operation */
    short sem_flg;          /* operation flags */
};
- `sem_num`:指定要操作的信号量在信号量集中的索引(从0开始)。
- `sem_op`:指定对信号量的操作。如果`sem_op`为正数,表示释放信号量,即增加信号量的值;如果`sem_op`为负数,表示获取信号量,即减少信号量的值。如果信号量的值小于`sem_op`的绝对值,进程将被阻塞,直到信号量的值足够。如果`sem_op`为0,表示等待信号量的值变为0。
- `sem_flg`:是一组操作标志位。常见的标志位有`IPC_NOWAIT`(如果操作不能立即完成,不等待,直接返回错误)和`SEM_UNDO`(如果进程异常终止,系统自动恢复信号量的值)。

3. nsops:指定struct sembuf数组中元素的个数,即要执行的操作数。

返回值

成功时,semop()返回0。失败时,返回-1,并设置errno来指示错误原因。例如,EAGAIN表示使用了IPC_NOWAIT标志且操作不能立即完成,EIDRM表示信号量集已被删除。

示例代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
#include <unistd.h>

#define SEM_KEY 1234
#define SEM_NUM 1

union semun {
    int val;                /* value for SETVAL */
    struct semid_ds *buf;   /* buffer for IPC_STAT, IPC_SET */
    unsigned short *array;  /* array for GETALL, SETALL */
    struct seminfo *__buf;  /* buffer for IPC_INFO (Linux-specific) */
};

int main() {
    int semid;
    struct sembuf sop;
    union semun arg;

    // 创建信号量集
    semid = semget(SEM_KEY, SEM_NUM, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget");
        exit(1);
    }

    // 初始化信号量的值为1
    arg.val = 1;
    if (semctl(semid, 0, SETVAL, arg) == -1) {
        perror("semctl SETVAL");
        exit(1);
    }

    // 获取信号量
    sop.sem_num = 0;
    sop.sem_op = -1;
    sop.sem_flg = SEM_UNDO;
    if (semop(semid, &sop, 1) == -1) {
        perror("semop get semaphore");
        exit(1);
    }
    printf("Got the semaphore. Critical section entered.\n");

    // 模拟临界区操作
    sleep(2);

    // 释放信号量
    sop.sem_op = 1;
    if (semop(semid, &sop, 1) == -1) {
        perror("semop release semaphore");
        exit(1);
    }
    printf("Released the semaphore. Critical section left.\n");

    // 删除信号量集
    if (semctl(semid, 0, IPC_RMID) == -1) {
        perror("semctl IPC_RMID");
        exit(1);
    }
    return 0;
}

在这段代码中,首先创建了一个信号量集并初始化其中的信号量值为1。然后通过semop()函数获取信号量,进入临界区,模拟在临界区的操作(这里使用sleep(2)模拟),最后释放信号量。如果操作过程中出现错误,通过perror()函数打印错误信息。

semctl函数详解

函数原型

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);

参数说明

  1. semid:是信号量集的标识符,由semget()函数返回。
  2. semnum:指定要操作的信号量在信号量集中的索引(从0开始)。如果cmdIPC_RMID等与整个信号量集相关的操作,semnum会被忽略。
  3. cmd:指定要执行的控制命令。常见的命令有:
    • SETVAL:用于初始化指定信号量的值,此时需要使用union semun联合体来传递初始化值。
    • GETVAL:用于获取指定信号量的值。
    • IPC_RMID:用于删除信号量集。
    • GETALL:用于获取信号量集中所有信号量的值,将值存储在union semun联合体的array成员中。
    • SETALL:用于设置信号量集中所有信号量的值,值由union semun联合体的array成员提供。
  4. ...:这是一个可变参数部分,具体取决于cmd的值。通常会使用union semun联合体来传递相关参数,union semun联合体定义如下:
union semun {
    int val;                /* value for SETVAL */
    struct semid_ds *buf;   /* buffer for IPC_STAT, IPC_SET */
    unsigned short *array;  /* array for GETALL, SETALL */
    struct seminfo *__buf;  /* buffer for IPC_INFO (Linux-specific) */
};

返回值

成功时,semctl()返回值取决于cmd。例如,GETVAL命令返回指定信号量的值,IPC_RMID命令成功时返回0。失败时,返回-1,并设置errno来指示错误原因。例如,EINVAL表示无效的semidsemnumcmd

示例代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>

#define SEM_KEY 1234
#define SEM_NUM 1

union semun {
    int val;                /* value for SETVAL */
    struct semid_ds *buf;   /* buffer for IPC_STAT, IPC_SET */
    unsigned short *array;  /* array for GETALL, SETALL */
    struct seminfo *__buf;  /* buffer for IPC_INFO (Linux-specific) */
};

int main() {
    int semid;
    union semun arg;
    int semval;

    // 创建信号量集
    semid = semget(SEM_KEY, SEM_NUM, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget");
        exit(1);
    }

    // 初始化信号量的值为5
    arg.val = 5;
    if (semctl(semid, 0, SETVAL, arg) == -1) {
        perror("semctl SETVAL");
        exit(1);
    }

    // 获取信号量的值
    semval = semctl(semid, 0, GETVAL);
    if (semval == -1) {
        perror("semctl GETVAL");
        exit(1);
    }
    printf("The value of the semaphore is: %d\n", semval);

    // 删除信号量集
    if (semctl(semid, 0, IPC_RMID) == -1) {
        perror("semctl IPC_RMID");
        exit(1);
    }
    return 0;
}

在这段代码中,先创建了一个信号量集,然后使用semctl()函数的SETVAL命令初始化信号量的值为5。接着通过GETVAL命令获取信号量的值并打印。最后使用IPC_RMID命令删除信号量集。

信号量在多进程中的应用示例

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
#include <unistd.h>
#include <wait.h>

#define SEM_KEY 1234
#define SEM_NUM 1

union semun {
    int val;                /* value for SETVAL */
    struct semid_ds *buf;   /* buffer for IPC_STAT, IPC_SET */
    unsigned short *array;  /* array for GETALL, SETALL */
    struct seminfo *__buf;  /* buffer for IPC_INFO (Linux-specific) */
};

void child_process(int semid) {
    struct sembuf sop;
    // 获取信号量
    sop.sem_num = 0;
    sop.sem_op = -1;
    sop.sem_flg = SEM_UNDO;
    if (semop(semid, &sop, 1) == -1) {
        perror("child semop get semaphore");
        exit(1);
    }
    printf("Child process entered critical section.\n");
    // 模拟临界区操作
    sleep(1);
    // 释放信号量
    sop.sem_op = 1;
    if (semop(semid, &sop, 1) == -1) {
        perror("child semop release semaphore");
        exit(1);
    }
    printf("Child process left critical section.\n");
}

int main() {
    int semid;
    union semun arg;
    pid_t pid;

    // 创建信号量集
    semid = semget(SEM_KEY, SEM_NUM, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget");
        exit(1);
    }

    // 初始化信号量的值为1
    arg.val = 1;
    if (semctl(semid, 0, SETVAL, arg) == -1) {
        perror("semctl SETVAL");
        exit(1);
    }

    // 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(1);
    } else if (pid == 0) {
        child_process(semid);
        exit(0);
    } else {
        // 父进程获取信号量
        struct sembuf sop;
        sop.sem_num = 0;
        sop.sem_op = -1;
        sop.sem_flg = SEM_UNDO;
        if (semop(semid, &sop, 1) == -1) {
            perror("parent semop get semaphore");
            exit(1);
        }
        printf("Parent process entered critical section.\n");
        // 模拟临界区操作
        sleep(1);
        // 释放信号量
        sop.sem_op = 1;
        if (semop(semid, &sop, 1) == -1) {
            perror("parent semop release semaphore");
            exit(1);
        }
        printf("Parent process left critical section.\n");
        // 等待子进程结束
        wait(NULL);
        // 删除信号量集
        if (semctl(semid, 0, IPC_RMID) == -1) {
            perror("semctl IPC_RMID");
            exit(1);
        }
    }
    return 0;
}

在这个示例中,创建了一个信号量集并初始化信号量值为1。然后通过fork()函数创建一个子进程。父进程和子进程都尝试获取信号量进入临界区,模拟在临界区的操作后释放信号量。通过信号量机制,保证了父子进程不会同时进入临界区,实现了进程间的同步。最后,父进程等待子进程结束后删除信号量集。

信号量操作中的错误处理

在使用信号量相关函数时,可能会遇到各种错误。常见的错误包括:

  1. 信号量集不存在:在使用semget()获取信号量集时,如果信号量集不存在且未使用IPC_CREAT标志,会返回ENOENT错误。此时需要检查key值是否正确,或者根据需求决定是否创建信号量集。
  2. 权限不足:如果设置的semflg权限不正确,可能会导致权限不足的错误。例如,在创建信号量集时设置的权限不允许当前用户访问,在使用semop()semctl()操作信号量集时就会返回EACCES错误。需要确保信号量集的权限设置符合实际需求。
  3. 无效的参数:在调用semctl()函数时,如果传入的semidsemnumcmd参数无效,会返回EINVAL错误。在调用函数前,需要仔细检查参数的正确性。
  4. 资源不足:系统资源有限,在创建信号量集或执行信号量操作时,可能会因为资源不足而失败。例如,系统中信号量集的数量达到上限,调用semget()创建新的信号量集时会返回ENOSPC错误。此时需要合理规划信号量的使用,或者调整系统参数以增加可用资源。

在编写代码时,要养成良好的错误处理习惯,通过检查函数的返回值并使用perror()等函数打印错误信息,以便及时发现和解决问题。

信号量与其他同步机制的比较

  1. 与互斥锁的比较
    • 相似性:二进制信号量和互斥锁都可以用于实现对共享资源的互斥访问,保证同一时间只有一个进程或线程可以进入临界区。
    • 区别:互斥锁通常用于线程间同步,而信号量既可以用于线程间同步,也可以用于进程间同步。信号量的值可以是任意非负整数,除了实现互斥功能外,还可以用于控制对多个共享资源实例的访问。而互斥锁只有锁定和解锁两种状态,值为0(锁定)或1(解锁)。
  2. 与条件变量的比较
    • 相似性:信号量和条件变量都可以用于线程或进程间的同步,都可以让线程或进程等待某个条件满足。
    • 区别:条件变量通常与互斥锁配合使用,线程在等待条件变量时会先释放互斥锁,当条件满足被唤醒后再重新获取互斥锁。而信号量通过计数器的值来控制访问,获取信号量时如果信号量值不足会阻塞,释放信号量时会增加信号量的值。条件变量更侧重于等待某个特定条件的变化,而信号量更侧重于控制资源的数量。
  3. 与读写锁的比较
    • 相似性:读写锁和信号量都可以用于控制对共享资源的访问,提高并发性能。
    • 区别:读写锁区分了读操作和写操作,允许多个线程同时进行读操作,但写操作必须是独占的。信号量则更通用,可以根据信号量的值来灵活控制对共享资源的访问方式。例如,通过设置信号量的值为1可以实现类似读写锁写操作的独占访问,设置为大于1的值可以控制多个读操作或其他类型的并发访问。

信号量在实际项目中的应用场景

  1. 资源池管理:在数据库连接池、线程池等资源池的实现中,信号量可以用于管理资源的分配和释放。例如,数据库连接池可以使用信号量来表示当前可用的连接数量,当一个线程需要获取数据库连接时,先获取信号量,如果信号量值为0,表示没有可用连接,线程等待。当线程使用完连接后,释放信号量,其他线程就可以获取连接。
  2. 进程间通信缓冲区管理:在进程间通过共享内存进行通信时,信号量可以用于控制对共享缓冲区的访问。例如,一个进程向共享缓冲区写入数据,另一个进程从共享缓冲区读取数据。可以使用两个信号量,一个用于表示缓冲区是否有数据可读取(初始值为0),另一个用于表示缓冲区是否有空间可写入(初始值为缓冲区大小)。写入进程在写入数据前先获取可写入信号量,写入完成后释放可读取信号量。读取进程在读取数据前先获取可读取信号量,读取完成后释放可写入信号量。
  3. 并发任务调度:在多任务并发执行的场景中,信号量可以用于控制任务的执行顺序和并发数量。例如,有多个任务需要执行,但系统资源有限,只能同时执行一定数量的任务。可以使用信号量来表示当前可用的任务执行槽位,每个任务在开始执行前获取信号量,执行完成后释放信号量。这样可以有效地控制任务的并发数量,避免系统资源过度消耗。

通过深入理解信号量的概念、相关函数的使用方法以及在不同场景中的应用,开发者可以在Linux C语言编程中更好地利用信号量机制来实现高效、安全的进程间或线程间同步。在实际应用中,需要根据具体的需求和场景,合理选择和使用信号量,并注意与其他同步机制的配合,以构建健壮的并发程序。同时,要重视错误处理,确保程序在各种情况下都能稳定运行。在处理复杂的并发场景时,可能还需要结合其他技术,如线程安全的数据结构、锁优化等,以进一步提升程序的性能和可靠性。