LinuxC语言并发程序笔记(第二十四天)

共享内存

特点

速度最快:数据直接映射到进程地址空间,零拷贝零内核切换
容量大:受物理内存 + ulimit 限制,可放大文件(shm_open 版)。
无内置同步:读写双方必须自行加锁(信号灯、互斥量等)。
生命周期随内核:除非显式 shmctl(IPC_RMID) 或重启,否则一直存在。

作用

大吞吐量、低延迟场景:图像帧、高速日志、AI 推理结果、数据库 buffer pool。
频繁交换大块数据且已另有同步手段的模块。

ftok – 生成 System V IPC 键值:

key_t ftok(const char *path, int proj_id);

成功返回key值,失败返回 -1。  
path 为随意一个地址;proj_id也是随便的值(只有前八位有效)。
path + id 会创建一个独一无二的键值,使后续的进程可以通过相同的参数创建一样的键值通信。

后续的通信方式都能遇到

shmget – 创建 / 获取共享内存:

int shmget(key_t key, size_t size, int shmflg);

成功返回 shmid,失败 -1。  
shmflg 创建的参数,常用组合有:  
- `IPC_CREAT | 0666`  不存在则创建,权限 0666  
- `IPC_CREAT | IPC_EXCL | 0666`  不存在则创建,已存在则返回 -1(排他创建)  
- `0`  仅打开已存在的段  
size 是共享内存的大小,可按需求分配
同一 key 多次调用都只会返回同一 id。


shmat – 投影共享内存:

void *shmat(int shmid, const void *shmaddr, int shmflg);

成功返回映射首地址,失败 (void *)-1。  
shmaddr 选择:  
- `NULL`  内核自动选地址(最常用)  
- `非零值`  用作映射起始地址(需页对齐,极少用)  
推荐为NULL,因为时间编程并不能知道哪里地址没用占用

shmflg 选项:  
- `0`  读写  
- `SHM_RDONLY`  只读  

shmdt – 解除投影:

int shmdt(const void *shmaddr);

成功 0,失败 -1。  
仅脱离当前进程,共享内存段仍存在内核中,所以重新推出前要写这个函数和下面的函数。

shmctl – 控制共享内存:

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

成功 0,失败 -1。  

cmd 可选:  
- `IPC_RMID`  标记销毁(引用计数为 0 时内核回收,退出程序和上面一个函数一起写)  
- `IPC_STAT`  取当前属性到 buf  
- `IPC_SET`   用 buf 改权限/属主  
- `SHM_LOCK`  锁定页不被换出(需 CAP_IPC_LOCK)  
- `SHM_UNLOCK` 解锁

示例:

common.h

#ifndef COMMON_H
#define COMMON_H

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

//给ftok使用
#define PATHNAME "."
#define PROJ_ID   66
#define SHM_SIZE  sizeof(shm_block_t)

typedef struct {
    sem_t sem_writer;
    sem_t sem_reader;
    int   counter;
} shm_block_t;

#endif

writer.h

#include "common.h"

int main(){
        key_t key;
        int shmid;
        if((key = ftok(PATHNAME,PROJ_ID)) < 0 ){
                perror("ftok");
                exit(-1);
        }
        if((shmid = shmget(key,SHM_SIZE,IPC_CREAT | 0666)) < 0){
                perror("shmget");
                exit(-1);
        }
        shm_block_t *shm;
        if((shm = (shm_block_t *)shmat(shmid,NULL,0)) == (void *)-1 ){
                perror("shmat");
                exit(-1);
        }
        if (sem_init(&shm->sem_writer, 1, 1) == -1) { perror("sem_init writer"); exit(-1); }
        if (sem_init(&shm->sem_reader, 1, 0) == -1) { perror("sem_init reader"); exit(-1); }

        shm->counter = 0;

        while(1){
                sem_wait(&shm->sem_writer);
                if(shm->counter == 10){
                        break;
                }
                shm->counter++;
                printf("writer: %d\n",shm->counter);
                sem_post(&shm->sem_reader);
                sleep(1);
        }

        shmdt(shm);
        shmctl(shmid,IPC_RMID,NULL);

        return 0;
}

reader.c

#include "common.h"

int main(){
        key_t key;
        int shmid;
        if((key = ftok(PATHNAME,PROJ_ID)) < 0 ){
                perror("ftok");
                exit(-1);
        }
        if((shmid = shmget(key,SHM_SIZE,IPC_CREAT | 0666)) < 0){
                perror("shmget");
                exit(-1);
        }
        shm_block_t *shm;
        if((shm = (shm_block_t *)shmat(shmid,NULL,0)) == (void *)-1 ){
                perror("shmat");
                exit(-1);
        }

        while(1){
                sem_wait(&shm->sem_reader);
                printf("reader: %d\n",shm->counter);
                sem_post(&shm->sem_writer);
                if(shm->counter == 10){
                        break;
                }
        }

        shmdt(shm);
        shmctl(shmid,IPC_RMID,NULL);

        return 0;
}

消息队列

特点

自带同步:发送/接收是原子操作,无需额外锁。
按类型取消息:支持优先级多路复用msgtyp > 0)。
数据拷贝两次:用户→内核→用户,速度低于共享内存
容量受限:单条长度 ≤ MSGMAX,队列总数 ≤ MSGMNI,内核参数可调。
生命周期随内核:msgctl(IPC_RMID) 后不再接收,但已读进程可继续读完。

作用

任务分发:父进程按类型投递子任务,多个 worker 取各自类型。
控制流通道:命令/应答短小消息,长度固定、频率中等的场景。

msgget – 创建 / 获取消息队列:

int msgget(key_t key, int msgflg);

成功返回队列id,失败 -1。  
msgflg 常用:  
- `IPC_CREAT | 0666`  不存在则创建  
- `IPC_CREAT | IPC_EXCL | 0666`  排他创建  
- `0`  仅打开已存在队列

msgsnd – 发送消息:

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);、

成功 0,失败 -1。
msgsz 是写入的大小
msgflg 选项:  
- `0`  阻塞直到可写  
- `IPC_NOWAIT`  队列满时立即返回 -1 并设 errno=EAGAIN  

msgrcv – 接收消息:

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

成功返回实际字节数,失败 -1。  
msgtyp 是接受的类型,如:sizeof(int)即4,就是整型。
msgflg 选项:  同上

msgctl – 控制消息队列:

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

成功 0,失败 -1。  
cmd 可选:  
- `IPC_RMID`  立即删除队列(仍在使用的进程可继续)  
- `IPC_STAT`  取属性到 buf  
- `IPC_SET`   用 buf 改权限、属主、最大消息数等

示例:

common.h

#ifndef COMMON_H
#define COMMON_H

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

//给ftok使用
#define PATHNAME "."
#define PROJ_ID   20
#define MSG_SIZE  64

typedef struct {
    long  mytpe;
    int   num;
} msg_t;

#endif

writer.c

#include "common.h"

int main(){
        key_t key;
        int msgid;
        if((key = ftok(PATHNAME,PROJ_ID)) < 0 ){
                perror("ftok");
                exit(-1);
        }
        if((msgid = msgget(key,IPC_CREAT | 0666)) < 0){
                perror("msgget");
                exit(-1);
        }

        msg_t reader = { .mytpe = 1 };
        msg_t writer = { 0 };

        for(int i = 1; ;i++){
                if(i == 11){
                        break;
                }
                reader.num = i;
                if (msgsnd(msgid, &reader, sizeof(int), 0) == -1)
                {
                        perror("msgsnd"); exit(1);
                }
                printf("writer sent: %d\n", i);

                if (msgrcv(msgid, &writer, sizeof(int), 2, 0) == -1)
                {
                        perror("msgrcv"); exit(1);
                }
                sleep(1);
        }

        msgctl(msgid,IPC_RMID,NULL);

        return 0;
}

reader.c

#include "common.h"
#include <time.h>

int main(){
        key_t key = ftok(PATHNAME, PROJ_ID);
        if (key == -1) { perror("ftok"); exit(-1); }

        int msgid = msgget(key, 0666);
        if (msgid == -1) { perror("msgget"); exit(-1); }

        msg_t reader = { 0 };
        msg_t writer = { .mytpe = 2 };

        while (1) {
                if (msgrcv(msgid, &reader, sizeof(int), 1, 0) == -1)
                { perror("msgrcv"); exit(-1); }
                time_t now = time(NULL);
                printf("[reader] 收到信息!num=%d  时间=%s", reader.num, ctime(&now));
                //回复空信息,让writer继续
                if (msgsnd(msgid, &writer, 0, 0) == -1)
                { perror("msgsnd ack"); exit(-1); }
                if(reader.num == 10){
                        break;
                }
        }

        msgctl(msgid,IPC_RMID,NULL);

        return 0;
}

信号灯集

特点

只做同步,不搬运数据:本质是计数器数组
原子批量操作:一次 semop 可同时对多个灯加减,全成功或全失败
支持“等待归零”:可实现资源耗尽屏障同步。
持久化:创建后一直存在,需显式 semctl(IPC_RMID) 删除。
开销最低:单个灯仅 2 字节,适合高频加解锁

作用

共享内存的配套锁:Ping-Pong 缓冲区、读写锁、环形队列指针。
多资源并发控制:连接池、缓冲区槽位、打印机票据序号。

semget – 创建 / 获取信号灯集:

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

成功返回 semid,失败 -1。  
nsems为信号灯的个数

semctl – 对单个或整集灯进行控制:

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

成功 0,失败 -1。  
第四参数 union semun 按 cmd 选用:  
- `SETVAL`  给第 semnum 灯赋初值(用 semun.val)  
- `SETALL`  给整集赋初值(用 semun.array)  
- `GETVAL`  取第 semnum 灯当前值  
- `GETALL`  取整集当前值  
- `IPC_RMID`  立即删除整个信号灯集  
- `IPC_STAT`  取属性到 semun.buf  
- `GETPID`  返回最后一次操作该灯的进程 PID  
- `GETNCNT`  返回等待该灯值增加的进程数  
- `GETZCNT`  返回等待该灯值归零的进程数

如果想删除信号灯就semctl(semid,0,IPC_RMID,NULL);

semop –  P/V 操作  :

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

成功 0,失败 -1。  
sops 数组元素 struct sembuf 字段为
- `sem_num`  灯编号(从0开始,到nsems-1)  
- `sem_op`:
 -1=P(等待资源), +1=V(释放资源), 0=等待归零(理论可以+2-2+3-3的,但是推荐都只操作1)
- `sem_flg`  0 / IPC_NOWAIT,表示阻塞或直接退出

nsops为操作的信号灯个数。

示例:

common.h

#ifndef COMMON_H
#define COMMON_H

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

#define PATHNAME "."
#define PROJ_ID   66
#define SHM_SIZE  1024          // 1 KB 足够

enum { SEM_WRITER, SEM_READER, SEM_NSEMS };

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

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

#endif

writer.c

#include "common.h"

int main(void)
{
    key_t key = ftok(PATHNAME, PROJ_ID);
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
    char *shm = (char *)shmat(shmid, NULL, 0);
    if (shm == (void *)-1) { perror("shmat"); exit(1); }

    int semid = semget(key, SEM_NSEMS, IPC_CREAT | 0666);
    unsigned short vals[SEM_NSEMS] = {1, 0};
    union semun { int val; struct semid_ds *buf; unsigned short *array; } su;
    su.array = vals;
    semctl(semid, 0, SETALL, su);

    while (1) {
        P(semid, SEM_WRITER);
        printf("writer> ");
        fflush(stdout);
        if (!fgets(shm, SHM_SIZE, stdin)) break;
        shm[strcspn(shm, "\n")] = '\0';

        if (strcmp(shm, "quit") == 0) {
            V(semid, SEM_READER);
            break;
        }
        V(semid, SEM_READER);
    }

    shmdt(shm);
    shmctl(shmid,IPC_RMID,NULL);
    semctl(semid,0,IPC_RMID,NULL);

    return 0;
}

reader.c

#include "common.h"

int main(void)
{
    key_t key = ftok(PATHNAME, PROJ_ID);
    int shmid = shmget(key, SHM_SIZE, 0666);
    char *shm = (char *)shmat(shmid, NULL, 0);
    if (shm == (void *)-1) { perror("shmat"); exit(1); }

    int semid = semget(key, SEM_NSEMS, 0666);

    while (1) {
        P(semid, SEM_READER);
        if (strcmp(shm, "quit") == 0) break;
        printf("reader 输出: \"%s\"  ,大小=%zu 字节\n", shm, strlen(shm));
        fflush(stdout);

        V(semid, SEM_WRITER);
    }

    shmdt(shm);
    shmctl(shmid,IPC_RMID,NULL);
    semctl(semid,0,IPC_RMID,NULL);

    return 0;
}

总结:

维度共享内存消息队列信号灯集
拷贝次数0(用户-用户直接)2(用户→内核→用户)0(仅计数)
系统调用频率仅 attach/detach每次收发各 1 次每次 PV 各 1 次
数据吞吐量最高中等无数据
单条上限≈ 物理内存MSGMAX(通常 8 KB)2 字节 × 灯数
批量原子不支持单条原子多灯原子

 

常见组合模式:

单生产者 + 单消费者(Ping-Pong)
共享内存(数据) + 2 个信号灯(空/满)

多生产者 + 单消费者
消息队列(type=1 表示任务)
消费者按 type 聚合或轮流读取;无需额外锁。

资源池管理
信号灯集初值 = 池大小
每次申请资源 P(-1),释放 V(+1);支持一次拿/放多个资源(sem_op ≠ ±1)。

零拷贝日志
共享内存环形缓冲
写指针、读指针各用 1 个灯保护;读线程写完指针后 V,写线程等待 P。

共享内存流程
一句话:先“拿地”,再“装修”,再“搬家”,最后“拆房子”。

拿地shmget:向内核申请一块物理内存,拿到门牌号 shmid
装修shmat:把这块内存挂到你家进程地址空间里,现在你能像普通数组一样用。
搬家 – 读/写:随便 strcpymemcpy,速度飞起,但记得先加信号灯“锁门”。
搬离shmdt:进程自己不用了,解除映射,房子还在。
拆房子shmctl(IPC_RMID):真正把内存归还给内核,别人再也打不开。

消息队列流程
一句话:先“开邮箱”,再“投信”,再“取信”,最后“注销邮箱”。

开邮箱msgget:内核给你建一个链表邮箱,返回 msqid
投信msgsnd:把你的消息(带类型标签)塞进链表尾,满了就排队。
取信msgrcv:按类型挑一条消息出来,没有就睡觉(可设不阻塞)。
注销邮箱msgctl(IPC_RMID):邮箱销毁,已投的信读完就消失。

信号灯集流程
一句话:先“造红绿灯”,再“设初始灯色”,再“日常过路口”,最后“拆红绿灯”。

造红绿灯semget:告诉内核要几个灯,拿到灯控器 semid
设灯色semctl(SETALL):一次性把每个灯设为红/绿数字(如 {1,0})。
过路口semop
        红灯 -1(P):灯值够就减,不够就排队等。
        绿灯 +1(V):释放资源,唤醒等待队列。
拆红绿灯semctl(IPC_RMID):灯控器销毁,所有等待进程立即报错返回。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值