【进程间通信(无/有名管道、信号、共享内存)】

Linux下进程间通信概述

常用的进程间通信方式

1、传统的进程间通信方式

无名管道(pipe)、有名管道(fifo)和信号(signal)

2、System V IPC对象

共享内存(share memory)、消息队列(message queue)和信号灯(semaphore)

3、BSD

套接字(socket)

无名管道

无名管道特点:

只能用于具有亲缘关系的进程之间的通信;(如父子进程、兄弟进程等)

半双工的通信模式,具有固定的读端和写端;

管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数。

管道创建与关闭

管道读写注意点

#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
    int fd[2] = {0};
    char buf[65536] = {0};
    if(pipe(fd) < 0)
    {
        perror("pipe err");
        return -1;
    }
    /*write(fd[1], "hello", 5);
    read(fd[0], buf, 5);
    printf("buf:%s\n", buf);
    read(fd[0], buf, 5);
    printf("buf:%s\n", buf);
    */
    write(fd[1],buf, 65536);
    read(fd[0], buf, 4095);
    printf("befor write\n");
    write(fd[1],"a", 1);
    printf("after write\n");
    printf("%d %d\n", fd[0], fd[1]);
    return 0;
}

练习:fork创建进程,父进程中循环从终端输入

子进程负责把数据打印到终端,当输入quit程序退出

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
    int fd[2] = {0};
    pid_t pid;
    char buf[32] = {0};
    ssize_t s;
    if (pipe(fd) < 0)
    {
        perror("pipe err");
        return -1;
    }
    if ((pid = fork()) < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0)
    {
        while (1)
        {
            s = read(fd[0], buf, 32);
            if (strncmp(buf, "quit", 4) == 0)
                break;
            printf("buf: %s", buf);
        }
        exit(0);
    }
    else
    {
        while (1)
        {
            fgets(buf, 32, stdin);
            write(fd[1], buf, strlen(buf)+1);
            if (strncmp(buf, "quit", 4) == 0)
                break;
        }
        wait(NULL);
    }
    return 0;
}

不使用strlen(buf)+1或者使用

memset(buf, 0, sizeof(buf));  // 清空自定义缓冲区buf

有名管道

FIFO

       无名管道只能用于具有亲缘关系的进程之间,这就限制了无名管道的使用范围。

       有名管道可以使互不相关的两个进程互相通信。有名管道可以通过路径名来指出,并且在文件系统中可见。

       进程通过文件IO来操作有名管道、有名管道遵循先进先出规则、不支持如lseek()操作。

还需要头文件 #include <sys/stat.h>

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main(char argc, char *argv[])
{
    int fd;
    char buf[32] = {0};
    if(mkfifo("./fifo", 0666) < 0)
    {
        if(errno == EEXIST)
            printf("fifo file exist\n");
        else
        {
        perror("mkfifo err");
        return -1;
        }
    }
    fd = open("./fifo", O_WRONLY);
    write(fd, "hello", 5);
    read(fd,buf,5);
    printf("%s\n",buf);
 
 
    printf("mkfifo success\n");
 
    return 0;
}

两个进程链接

read.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main(char argc, char *argv[])
{
    int fd;
    char buf[32] = {0};
    if(mkfifo("./fifo", 0666) < 0)
    {
        if(errno == EEXIST)
            printf("fifo file exist\n");
        else
        {
        perror("mkfifo err");
        return -1;
        }
    }
    fd = open("./fifo", O_WRONLY);
    write(fd, "hello", 5);
    read(fd,buf,5);
    printf("%s\n",buf);
 
 
    printf("mkfifo success\n");
 
    return 0;
}

write.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main(char argc, char *argv[])
{
    int fd;
    char buf[32] = {0};
    if(mkfifo("./fifo", 0666) < 0)
    {
        if(errno == EEXIST)
            printf("fifo file exist\n");
        else
        {
        perror("mkfifo err");
        return -1;
        }
    }
    fd = open("./fifo", O_WRONLY);
    write(fd, "hello", 5);
    read(fd,buf,5);
    printf("%s\n",buf);
 
 
    printf("mkfifo success\n");
 
    return 0;
}

信号

信号通信

使用 kill -l 查看信号

信号发送与捕捉

kill()和raise()

        kill函数同读者熟知的kill系统命令一样,可以发送信号给进程或进程组(实际上,kill系统命令只是kill函数的一个用户接口)。

        kill–l命令查看系统支持的信号列表。

        raise函数允许进程向自己发送信号

alarm()和pause()

        alarm()也称为闹钟函数,它可以在进程中设置一个定时器。当定时器指定的时间到时,内核就向进程发送SIGALARM信号。(以秒为单位,若设置多个定时器,按最后一个执行)

        pause()函数是用于将调用进程挂起直到收到信号为止。

#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
    /*
    //kill(getpid(),SIGINT);
    raise(SIGINT);
    while(1);
    */
 
   printf("%d\n",alarm(3)); //第一次调用返回0
   //sleep(10);
   printf("%d\n",alarm(2));//多次调用函数,返回上一次定时剩余时间
   pause(); //将进程挂起,直到收到信号终止
   pause(); //将进程挂起,直到收到信号终止
    return 0;
}

信号的处理

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
void handler(int sig)
{
    if (sig == SIGINT)
        printf("ctrl+c\n");
    else if (sig == SIGQUIT)
        printf("ctrl+/\n");
}
int main(int argc, char *argv[])
{
    // 1.忽略信号:当前进程不做任何处理
    //  signal(SIGINT, SIG_IGN);
    // 2.执行缺省(默认)操作:
    //  signal(SIGINT, SIG_DFL);
    // 3.捕捉信号:自定义函数去处理
    pid_t pid;
    pid = fork();
    if (pid == 0)
    {
        //发送信号
        kill(getppid(), SIGINT);
    }
    else if (pid > 0)
    {
        signal(SIGINT, handler);
        signal(SIGQUIT, handler);
    }
    pause();
    return 0;
}

练习:

    用信号的知识实现司机和售票员问题。

    1)售票员捕捉SIGINT(代表开车)信号,向司机发送SIGUSR1信号,司机打印(1et's gogogo)

    2)售票员捕捉SIGQUIT(代表停车)信号,向司机发送SIGUSR2信号,司机打印(stop the bus)

    3)司机捕捉SIGTSTP(代表到达终点站)信号,向售票员发送SIGUSR1信号,售票员打印(please get off the bus)

    4)司机等待售票员下车,之后司机再下车。

#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
pid_t pid;
void handler_saler(int sig)
{
    if(sig == SIGINT)
        kill(getppid(),SIGUSR1);
    else if(sig == SIGQUIT)
        kill(getppid(),SIGUSR2);
    else if(sig == SIGUSR1)
    {
        printf("please get off the bus\n");
        exit(0);
    }
 
}
void handler_driver(int sig)
{
    if(sig == SIGUSR1)
        printf("let's gogogo\n");
    else if(sig == SIGUSR2)
        printf("stop the bus\n");
    else if(sig == SIGTSTP)
        kill(pid,SIGUSR1);
}
int main(int argc, char *argv[])
{
    if((pid = fork()) < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0)
    {
        signal(SIGINT, handler_saler);
        signal(SIGQUIT,handler_saler);   
        signal(SIGUSR1,handler_saler); 
        signal(SIGTSTP,SIG_IGN);
    }
    else
    {
        signal(SIGUSR1, handler_driver);
        signal(SIGUSR2,handler_driver);
        signal(SIGTSTP,handler_driver);
        signal(SIGINT,SIG_IGN);
        signal(SIGQUIT,SIG_IGN);
        wait(NULL);
        exit(0);
    }
    while(1)
    {
        pause();
    }
    return 0;
}

共享内存

        共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝;

        为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间;进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。

        由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等。

共享内存的使用包括如下步骤:

  1. 生成 key 值:使用ftok(pathname, proj_id)生成唯一标识共享内存的 key,若失败则返回 - 1。
  2. 创建 / 打开共享内存:通过shmget(key, size, shmflg)创建新段或打开已有段,成功返回 shmid(段标识符)。
  3. 映射内存到进程地址空间:调用shmat(shmid, shmaddr, shmflg)将共享内存映射到当前进程,返回映射地址(可读写或只读)。
  4. 数据读写:进程直接操作映射后的内存地址(如指针解引用)。
  5. 撤销映射:通信完成后,用shmdt(shmaddr)解除进程与共享内存的关联。
  6. 删除共享内存对象:调用shmctl(shmid, IPC_RMID, NULL)删除内存段,避免资源泄露。

一、ftok () 函数

1. 函数原型

#include <sys/types.h>
#include <sys/ipc.h>  // 必须包含的头文件

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

2. 参数说明

参数名类型作用
pathnameconst char*一个已存在的文件路径(可以是普通文件或目录,建议用绝对路径,如 /tmp/share.tmp)。
proj_idint项目 ID(通常取 1~255 之间的非 0 整数,如 123、'A'),作为区分不同 IPC 对象的标识。

3. 返回值

  • 成功:返回一个非负的 key_t 类型键值(用于后续 shmget()msgget()semget() 等函数)。
  • 失败:返回 -1,并设置 errno(可通过 perror() 打印错误信息)。

ftok() 失败时会设置 errno,常见错误类型:

errno 值错误原因解决方案
ENOENTpathname 指定的文件不存在检查路径是否正确,确保文件已创建
EACCES无权限访问 pathname 文件确保进程对文件有读权限(chmod +r 文件名
ENOTDIRpathname 是目录,但权限不足检查目录的执行权限(chmod +x 目录名

二、shmget()函数

1.函数原型

#include <sys/types.h>   // 定义 key_t、size_t 等类型
#include <sys/ipc.h>     // 定义 IPC_* 相关宏(如 IPC_CREAT)
#include <sys/shm.h>     // 声明 shmget() 函数

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

2.参数说明

1. key_t key:共享内存的 “唯一标识”

  • 作用:进程间通过相同的 key 识别同一个共享内存(类似 “门锁钥匙”)。
  • 来源:
    • 由 ftok() 生成(推荐,多进程共享场景):如 key = ftok("/tmp/share.tmp", 123)

    • 固定值(如 0x12345678,需手动确保唯一性);

    • 特殊值 IPC_PRIVATE:创建 “私有共享内存”,仅能由创建进程的父子进程共享(无需 ftok(),避免键值冲突)。

2. size_t size:共享内存的大小(单位:字节)

  • 作用:指定要创建的共享内存的容量,若为 “打开已存在的共享内存”,size 可设为 0(忽略大小检查)。
  • 关键要求:
    • 必须是 系统页大小(page size)的整数倍(内核按 “页” 分配内存,页大小通常为 4KB,可通过 getpagesize() 获取);
    • 若 size 不是页大小的整数倍,内核会自动向上对齐到最近的页大小(例如:size=5000 字节,页大小 = 4096,实际分配 8192 字节,浪费 3192 字节);
    • 最小 size 为 1 页(4096 字节)

3. int shmflg:操作标志(创建 / 打开模式 + 权限控制)

  • 核心是 “标志组合”(用 | 连接),分为两类:
    • 「创建 / 打开标志」:控制函数行为是 “创建” 还是 “打开”;
    • 「权限标志」:控制共享内存的访问权限(与文件权限规则一致)。

(1)创建 / 打开标志(常用 2 个)

标志作用
IPC_CREAT若 key 对应的共享内存不存在,则创建;若已存在,则直接打开(“创建或打开”)。
IPC_EXCL仅与 IPC_CREAT 配合使用:若 key 对应的共享内存已存在,则返回 -1(确保创建 “全新” 的共享内存,避免误打开旧内存)。
IPC_NOWAIT(少用)若共享内存正在被其他进程占用,不阻塞等待,直接返回错误。

(2)权限标志(与文件权限一致,常用 0666

(3)常见标志组合示例

组合效果
`IPC_CREAT0666`创建或打开共享内存,所有用户可读写(最常用)。
`IPC_CREATIPC_EXCL0666`强制创建新共享内存,若已存在则报错(避免覆盖旧数据)。
0(无标志)仅打开已存在的共享内存,若不存在则报错。

3.返回值与错误处理

1. 返回值

  • 成功:返回非负整数 shmid(共享内存唯一标识符,后续操作的核心句柄);
  • 失败:返回 -1,并设置 errno(可通过 perror() 或 strerror(errno) 查看错误原因)。

2. 常见错误类型(errno)

errno 值错误原因解决方案
ENOENT未指定 IPC_CREAT,且 key 对应的共享内存不存在检查 key 是否正确,或添加 IPC_CREAT 标志
EEXIST指定 `IPC_CREATIPC_EXCL,但 key` 对应的共享内存已存在更换 key 或 proj_id(ftok 的参数),或删除旧共享内存(ipcrm -m shmid
EINVALsize 超过系统限制,或为负数减小 size(参考 /proc/sys/kernel/shmmax 查看最大限制)
EACCES共享内存已存在,但当前进程无访问权限检查 shmflg 中的权限标志(如改为 0666),或修改共享内存权限(ipcs -m 查看,ipcrm 删除后重建)
ENOMEM系统内存不足,无法分配共享内存释放系统内存,或减小 size

三、shmat()函数

1.函数原型

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

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

1.参数说明

参数含义
shmid共享内存段的标识符(由 shmget() 函数创建共享内存时返回)。
shmaddr

指定共享内存附加到进程地址空间的起始地址

- 设为 NULL:由系统自动分配一个合适的地址(推荐用法);

- 设为非 NULL:需结合 shmflg 的 SHM_RND 标志使用(手动指定地址,不推荐)。

shmflg

附加标志,控制附加行为和权限:

0:默认权限(读写权限由共享内存段本身的权限决定);

SHM_RDONLY:以只读模式附加(进程只能读取共享内存,不能修改);

SHM_RND:与 shmaddr 配合,将 shmaddr 向下取整为共享内存页大小的整数倍(仅当 shmaddr 非 NULL 时有效)。

3.返回值

  • 成功:返回共享内存段在当前进程地址空间中的起始地址void* 类型,需强制转换为实际数据类型使用);
  • 失败:返回 (void*)-1,并设置 errno 标识错误原因。

常见错误码(errno

错误码含义
EINVALshmid 无效,或 shmaddr 非 NULL 且不符合地址对齐要求,或权限不足。
EACCES进程没有权限附加该共享内存段(如共享内存是只读的,但进程以读写模式附加)。
ENOMEM系统内存不足,无法为共享内存分配虚拟地址空间。
EIDRM共享内存段已被标记为删除(shmctl(shmid, IPC_RMID, ...))。

四、shmdt()函数

1.函数原型

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

int shmdt(const void *shmaddr);

2.参数说明

参数含义
shmaddr共享内存段在当前进程地址空间中的起始地址(必须是 shmat() 成功返回的有效地址,不能是 NULL 或其他无效地址)。

3.返回值

  • 成功:返回 0,表示进程已成功与共享内存段分离;
  • 失败:返回 -1,并设置 errno 标识错误原因。

常见错误码(errno

错误码含义
EINVAL无效的 shmaddr(如地址未对齐、不是当前进程附加的共享内存地址,或该地址已被分离过)。
EIDRM共享内存段已被 shmctl(shmid, IPC_RMID, ...) 标记为删除(但未完全释放,仍可分离)。

五、shmctl()函数

1.函数原型

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

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

2.参数说明

参数含义
shmid共享内存段的标识符(由 shmget() 返回,唯一标识一个共享内存段)。
cmd控制命令(核心参数),指定对共享内存执行的操作(常用命令见下文详解)。
buf

指向 struct shmid_ds 结构体的指针,用于存储 / 传递共享内存的属性信息:

- 若 cmd 是 “查询 / 修改” 类命令(如 IPC_STAT/IPC_SET),buf 需指向有效结构体;

- 若 cmd 是 “删除 / 锁定” 类命令(如 IPC_RMID/SHM_LOCK),buf 可设为 NULL

3.核心控制命令(cmd 参数详解)

shmctl() 的功能完全由 cmd 决定,以下是最常用的 5 个命令,覆盖绝大多数场景:

命令功能描述buf 要求权限要求
IPC_STAT查询共享内存属性:将 shmid 对应的共享内存属性写入 buf 指向的 struct shmid_ds必须非 NULL(需提前分配内存)进程需有读取权限(如 0444
IPC_SET修改共享内存属性:用 buf 中的值更新共享内存的属性(仅允许修改 shm_perm.uidshm_perm.gidshm_perm.mode)。必须非 NULL(需填充要修改的字段)进程需是共享内存所有者,或 root 权限
IPC_RMID标记共享内存为删除:触发共享内存的 “延迟删除”(核心命令)。设为 NULL 即可同 IPC_SET(所有者或 root)
SHM_LOCK锁定共享内存:禁止系统将该共享内存换出到交换分区(提升访问效率)。设为 NULL 即可仅 root 或创建者可用
SHM_UNLOCK解锁共享内存:取消 SHM_LOCK 的锁定效果。设为 NULL 即可仅 root 或创建者可用
关键命令补充说明(IPC_RMID 重点!)
  • IPC_RMID 不会立即删除共享内存,仅将其标记为 “待删除”;
  • 只有当所有附加该共享内存的进程都调用 shmdt() 分离后,系统才会真正释放共享内存的资源;
  • 标记后,新的进程无法再通过 shmat() 附加该共享内存(会返回 EIDRM 错误);
  • 若进程未分离就退出,系统会自动为其分离,最终触发共享内存释放。

4.返回值

  • 成功:返回 0(若 cmd 是 IPC_STAT,则 buf 已填充属性信息);
  • 失败:返回 -1,并设置 errno 标识错误原因。

常见错误码(errno

错误码含义
EINVALshmid 无效,或 cmd 是非法命令,或 buf 为 NULL 但 cmd 要求非 NULL
EACCES进程无权限执行该命令(如非所有者调用 IPC_SET,或无读取权限调用 IPC_STAT)。
EIDRM共享内存已被 IPC_RMID 标记为删除。
EPERM无足够权限(如普通用户调用 SHM_LOCK)。
ENOMEM调用 IPC_STAT 时,buf 指向的内存空间不足(极少出现)。
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
    key_t key;
    int shmid;
    char *p = NULL;
    key = ftok("./app", 'a');
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    // 1.创建共享内存
    shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid < 0)
    {
        if (errno == EEXIST)
            shmid = shmget(key, 128, 0666);
        else
        {
            perror("shmget err");
            return -1;
        }
    }
 
    // 2.映射,将共享内存地址映射到用户空间
    // 参数:NULL:让系统自动完成映射,  0:可读可写
    p = (char *)shmat(shmid, NULL, 0);
    if (p == (char *)-1)
    {
        perror("shmat err");
        return -1;
    }
    fgets(p, 32, stdin);
    printf("p:%s", p);
 
    printf("shmid:%d\n", shmid);
    printf("%#x\n", key);
 
    //3.取消映射
    shmdt(p);
    //4.删除共享内存
    shmctl(shmid, IPC_RMID, NULL);
    system("ipcs -m");
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值