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

Linux C语言共享内存的同步机制

2023-10-234.6k 阅读

共享内存概述

在Linux系统中,共享内存是一种高效的进程间通信(IPC)机制。它允许多个进程共享同一块物理内存区域,从而实现数据的快速交换和共享。与其他IPC机制(如管道、消息队列等)相比,共享内存不需要在进程之间进行数据的复制,因此具有较高的性能。

共享内存的基本原理是,操作系统在物理内存中分配一块内存区域,并将其映射到多个进程的地址空间中。这样,不同的进程就可以通过访问各自地址空间中的这块共享内存区域来进行数据的读写操作。然而,由于多个进程可以同时访问共享内存,因此需要引入同步机制来确保数据的一致性和避免竞争条件。

共享内存的创建与映射

在Linux C语言中,使用系统调用shmget来创建共享内存段,使用shmat将共享内存段映射到进程的地址空间。

shmget函数

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

int shmget(key_t key, size_t size, int shmflg);
  • key:是一个key_t类型的键值,用于唯一标识共享内存段。通常可以使用ftok函数生成。
  • size:指定共享内存段的大小,以字节为单位。
  • shmflg:是一组标志位,用于指定共享内存的创建和访问权限。例如,IPC_CREAT表示如果共享内存段不存在则创建它,0666表示设置读写权限。

shmat函数

#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmid:是shmget函数返回的共享内存标识符。
  • shmaddr:指定映射到进程地址空间的地址。通常设为NULL,让系统自动选择合适的地址。
  • shmflg:标志位,如SHM_RDONLY表示以只读方式映射。

示例代码:

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

#define SHM_SIZE 1024

int main() {
    key_t key;
    int shmid;
    char *shm, *s;

    // 生成键值
    if ((key = ftok(".", 'a')) == -1) {
        perror("ftok");
        exit(1);
    }

    // 创建共享内存段
    if ((shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666)) == -1) {
        perror("shmget");
        exit(1);
    }

    // 映射共享内存段到进程地址空间
    if ((shm = (char *)shmat(shmid, NULL, 0)) == (void *)-1) {
        perror("shmat");
        exit(1);
    }

    // 写入数据到共享内存
    s = shm;
    for (int i = 0; i < SHM_SIZE; i++) {
        *s++ = 'a' + i % 26;
    }

    // 分离共享内存
    if (shmdt(shm) == -1) {
        perror("shmdt");
        exit(1);
    }

    return 0;
}

同步机制的必要性

共享内存本身并没有提供同步机制。当多个进程同时访问和修改共享内存中的数据时,可能会出现竞争条件(race condition)。例如,一个进程正在读取共享内存中的数据,而另一个进程同时对该数据进行修改,这可能导致读取到的数据不一致或错误。

为了避免这些问题,需要引入同步机制来协调多个进程对共享内存的访问。常见的同步机制包括信号量(Semaphore)、互斥锁(Mutex)和条件变量(Condition Variable)等。

信号量同步机制

信号量是一种计数器,用于控制对共享资源的访问。在Linux C语言中,使用系统调用semgetsemopsemctl来操作信号量。

semget函数

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

int semget(key_t key, int nsems, int semflg);
  • key:与共享内存类似,用于唯一标识信号量集。
  • nsems:指定信号量集中信号量的个数。
  • semflg:标志位,如IPC_CREAT等。

semop函数

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

int semop(int semid, struct sembuf *sops, size_t nsops);
  • semidsemget返回的信号量标识符。
  • sops:是一个指向struct sembuf结构体数组的指针,用于指定信号量操作。
  • nsops:指定struct sembuf结构体数组的元素个数。

struct sembuf结构体定义如下:

struct sembuf {
    unsigned short sem_num;  /* semaphore number */
    short sem_op;           /* semaphore operation */
    short sem_flg;          /* operation flags */
};
  • sem_num:指定要操作的信号量在信号量集中的编号。
  • sem_op:指定信号量操作。例如,1表示释放信号量(增加计数器),-1表示获取信号量(减少计数器)。
  • sem_flg:标志位,如SEM_UNDO表示在进程终止时自动撤销操作。

semctl函数

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

int semctl(int semid, int semnum, int cmd, ...);
  • semid:信号量标识符。
  • semnum:要操作的信号量编号。
  • cmd:指定操作命令,如SETVAL用于初始化信号量的值。

示例代码:

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

#define SHM_SIZE 1024
#define SEM_KEY 1234
#define SHM_KEY 5678

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 semaphore_p(int semid) {
    struct sembuf sem_op;
    sem_op.sem_num = 0;
    sem_op.sem_op = -1;
    sem_op.sem_flg = SEM_UNDO;
    if (semop(semid, &sem_op, 1) == -1) {
        perror("semaphore_p");
        exit(1);
    }
}

void semaphore_v(int semid) {
    struct sembuf sem_op;
    sem_op.sem_num = 0;
    sem_op.sem_op = 1;
    sem_op.sem_flg = SEM_UNDO;
    if (semop(semid, &sem_op, 1) == -1) {
        perror("semaphore_v");
        exit(1);
    }
}

int main() {
    key_t shm_key, sem_key;
    int shmid, semid;
    char *shm, *s;
    union semun sem_union;

    // 生成共享内存和信号量的键值
    if ((shm_key = ftok(".", 'a')) == -1) {
        perror("ftok for shm");
        exit(1);
    }
    if ((sem_key = ftok(".", 'b')) == -1) {
        perror("ftok for sem");
        exit(1);
    }

    // 创建共享内存段
    if ((shmid = shmget(shm_key, SHM_SIZE, IPC_CREAT | 0666)) == -1) {
        perror("shmget");
        exit(1);
    }

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

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

    // 映射共享内存段到进程地址空间
    if ((shm = (char *)shmat(shmid, NULL, 0)) == (void *)-1) {
        perror("shmat");
        exit(1);
    }

    // 父进程写入数据
    if (fork() == 0) {
        semaphore_p(semid);
        s = shm;
        for (int i = 0; i < SHM_SIZE; i++) {
            *s++ = 'a' + i % 26;
        }
        semaphore_v(semid);
    } else {
        // 父进程等待子进程完成写入
        wait(NULL);
        semaphore_p(semid);
        s = shm;
        for (int i = 0; i < SHM_SIZE; i++) {
            printf("%c", *s++);
        }
        printf("\n");
        semaphore_v(semid);
    }

    // 分离共享内存
    if (shmdt(shm) == -1) {
        perror("shmdt");
        exit(1);
    }

    // 删除共享内存段和信号量集
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl IPC_RMID");
        exit(1);
    }
    if (semctl(semid, 0, IPC_RMID, 0) == -1) {
        perror("semctl IPC_RMID");
        exit(1);
    }

    return 0;
}

互斥锁同步机制

互斥锁(Mutex)是一种特殊的二元信号量,其值只能是0或1。它用于保证在同一时刻只有一个进程能够访问共享资源。在Linux C语言中,可以使用POSIX线程库(pthread)来实现互斥锁。

初始化互斥锁

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • mutex:指向要初始化的互斥锁变量。
  • attr:指向互斥锁属性结构体,通常设为NULL使用默认属性。

锁定互斥锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
  • mutex:指向要锁定的互斥锁变量。如果互斥锁已经被锁定,调用线程将被阻塞,直到互斥锁被解锁。

解锁互斥锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • mutex:指向要解锁的互斥锁变量。

示例代码:

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

#define SHM_SIZE 1024

void *write_to_shm(void *arg) {
    int *shmid = (int *)arg;
    char *shm, *s;
    pthread_mutex_t *mutex = (pthread_mutex_t *)shmat(*shmid, NULL, 0);
    shm = shmat(*shmid, (void *)(mutex + 1), 0);
    if (shm == (void *)-1) {
        perror("shmat for data");
        pthread_exit(NULL);
    }
    pthread_mutex_lock(mutex);
    s = shm;
    for (int i = 0; i < SHM_SIZE; i++) {
        *s++ = 'a' + i % 26;
    }
    pthread_mutex_unlock(mutex);
    if (shmdt(shm) == -1) {
        perror("shmdt for data");
    }
    if (shmdt(mutex) == -1) {
        perror("shmdt for mutex");
    }
    pthread_exit(NULL);
}

int main() {
    key_t key;
    int shmid;
    char *shm, *s;
    pthread_t tid;
    pthread_mutex_t *mutex;

    // 生成键值
    if ((key = ftok(".", 'a')) == -1) {
        perror("ftok");
        exit(1);
    }

    // 创建共享内存段,为互斥锁和数据预留空间
    if ((shmid = shmget(key, sizeof(pthread_mutex_t) + SHM_SIZE, IPC_CREAT | 0666)) == -1) {
        perror("shmget");
        exit(1);
    }

    // 映射共享内存段到进程地址空间,先映射互斥锁
    mutex = (pthread_mutex_t *)shmat(shmid, NULL, 0);
    if (mutex == (void *)-1) {
        perror("shmat for mutex");
        exit(1);
    }
    // 初始化互斥锁
    if (pthread_mutex_init(mutex, NULL) != 0) {
        perror("pthread_mutex_init");
        exit(1);
    }

    // 创建线程写入数据
    if (pthread_create(&tid, NULL, write_to_shm, &shmid) != 0) {
        perror("pthread_create");
        exit(1);
    }

    // 主线程等待线程完成写入
    if (pthread_join(tid, NULL) != 0) {
        perror("pthread_join");
        exit(1);
    }

    // 主线程读取数据
    shm = shmat(shmid, (void *)(mutex + 1), 0);
    if (shm == (void *)-1) {
        perror("shmat for data");
        exit(1);
    }
    pthread_mutex_lock(mutex);
    s = shm;
    for (int i = 0; i < SHM_SIZE; i++) {
        printf("%c", *s++);
    }
    printf("\n");
    pthread_mutex_unlock(mutex);

    // 分离共享内存
    if (shmdt(shm) == -1) {
        perror("shmdt for data");
        exit(1);
    }
    if (shmdt(mutex) == -1) {
        perror("shmdt for mutex");
        exit(1);
    }

    // 删除共享内存段
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl IPC_RMID");
        exit(1);
    }

    return 0;
}

条件变量同步机制

条件变量用于线程之间的同步,它通常与互斥锁一起使用。当某个条件满足时,一个线程可以通过条件变量通知其他等待的线程。

初始化条件变量

#include <pthread.h>

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
  • cond:指向要初始化的条件变量。
  • attr:指向条件变量属性结构体,通常设为NULL

等待条件变量

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
  • cond:指向要等待的条件变量。
  • mutex:指向与条件变量关联的互斥锁。在调用pthread_cond_wait前,必须先锁定该互斥锁。调用该函数时,互斥锁会被自动解锁,线程进入等待状态。当条件变量被唤醒时,互斥锁会被重新锁定。

唤醒条件变量

int pthread_cond_signal(pthread_cond_t *cond);
  • cond:指向要唤醒的条件变量。该函数会唤醒一个等待在该条件变量上的线程。

示例代码:

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

#define SHM_SIZE 1024

typedef struct {
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    int data_ready;
    char data[SHM_SIZE];
} SharedData;

void *write_to_shm(void *arg) {
    SharedData *shared = (SharedData *)arg;
    pthread_mutex_lock(&shared->mutex);
    for (int i = 0; i < SHM_SIZE; i++) {
        shared->data[i] = 'a' + i % 26;
    }
    shared->data_ready = 1;
    pthread_cond_signal(&shared->cond);
    pthread_mutex_unlock(&shared->mutex);
    pthread_exit(NULL);
}

int main() {
    key_t key;
    int shmid;
    SharedData *shared;
    pthread_t tid;

    // 生成键值
    if ((key = ftok(".", 'a')) == -1) {
        perror("ftok");
        exit(1);
    }

    // 创建共享内存段
    if ((shmid = shmget(key, sizeof(SharedData), IPC_CREAT | 0666)) == -1) {
        perror("shmget");
        exit(1);
    }

    // 映射共享内存段到进程地址空间
    shared = (SharedData *)shmat(shmid, NULL, 0);
    if (shared == (void *)-1) {
        perror("shmat");
        exit(1);
    }

    // 初始化互斥锁和条件变量
    pthread_mutex_init(&shared->mutex, NULL);
    pthread_cond_init(&shared->cond, NULL);

    // 创建线程写入数据
    if (pthread_create(&tid, NULL, write_to_shm, shared) != 0) {
        perror("pthread_create");
        exit(1);
    }

    // 主线程等待数据准备好
    pthread_mutex_lock(&shared->mutex);
    while (!shared->data_ready) {
        pthread_cond_wait(&shared->cond, &shared->mutex);
    }
    for (int i = 0; i < SHM_SIZE; i++) {
        printf("%c", shared->data[i]);
    }
    printf("\n");
    pthread_mutex_unlock(&shared->mutex);

    // 等待线程结束
    if (pthread_join(tid, NULL) != 0) {
        perror("pthread_join");
        exit(1);
    }

    // 清理
    pthread_cond_destroy(&shared->cond);
    pthread_mutex_destroy(&shared->mutex);
    if (shmdt(shared) == -1) {
        perror("shmdt");
        exit(1);
    }
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl IPC_RMID");
        exit(1);
    }

    return 0;
}

同步机制的选择

在实际应用中,选择合适的同步机制非常重要。信号量适用于控制对多个共享资源的访问,并且可以实现更复杂的同步逻辑。互斥锁则简单直接,适用于保证同一时刻只有一个进程访问共享资源。条件变量通常与互斥锁一起使用,用于线程之间基于条件的同步。

如果共享资源的访问模式较为简单,只是需要保证互斥访问,互斥锁可能是一个不错的选择。如果需要控制多个共享资源的并发访问,或者需要实现复杂的同步逻辑,信号量可能更合适。而当需要根据某个条件进行线程间的同步时,条件变量则是必不可少的。

同时,还需要考虑性能因素。例如,互斥锁的开销相对较小,而信号量的操作可能涉及更多的系统调用,开销相对较大。在高并发场景下,需要仔细评估不同同步机制对性能的影响。

综上所述,在Linux C语言中使用共享内存进行进程间通信时,同步机制是确保数据一致性和程序正确性的关键。通过合理选择和使用信号量、互斥锁和条件变量等同步机制,可以有效地解决共享内存访问中的竞争条件问题,实现高效、可靠的进程间通信。