IPC进程间通信 interprocess communicate

发布于:2025-07-18 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

1. IPC概述

IPC 进程间通信 interprocess communicate
进程间通信的原理:a和b是独立的,所以要找到一个公共的空间,把信息存储在这个空间上,在内核空间上,开一片内存区域,a和b都可以找到这个公共的区域

2. IPC三大通信方式分类

三大类:古老的通信方式,ipc对象通信、socket通信

2.1 古老的通信方式

无名管道、有名管道、信号(siginal)

2.2 IPC对象通信

system v、BSD、suse fedora、kernel.org、unix
消息队列(用的相对少,这里不讨论)
共享内存
信号量集

2.3 socket通信

网络通信(不同主机之间的通信)

线程信号,posix sem_init

特列:

  1. 古老的通信方式中信号是唯一的异步通信
  2. 所有的通信方式中共享内存是唯一的最高效

3. 管道通信详解

根据使用场景来区分:
管道分类:

  • 无名管道(pipe):只能给有亲缘关系进程通信
  • 有名管道(fifo):可以给任意单机进程通信

3.1 管道基本特性

  1. 管道是半双工的工作模式
  2. 所有管道都是特殊的文件不支持定位操作。lseek->> fd fseek ->>FILE*
  3. 管道是特殊文件,读写使用文件IO。fgets,fread,fgetc,open,read,write,close

管道使用注意事项:

  1. 读端存在,一直向管道中去写,超过64k,写会阻塞。
  2. 写端存在,读管道,如果管道为空,读会阻塞。(有数据的话就取出来)
  3. 管道破裂:读端关闭,写管道。是写端破裂了
  4. read 0:写端关闭,如果管道没有内容,read 0。
    如果
    使用框架:
    创建管道 → 读写管道 → 关闭管道
    代码举例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int	main(int argc, char **argv)
{
    int fd[2]={0};
    int ret = pipe(fd);
    pid_t pid = fork();
    if(pid>0)
    {

        //父进程写管道,不读.所以关闭管道的读段.
        close(fd[0]);
        char buf[]="hello";
        sleep(3);
        write(fd[1],buf,strlen(buf));
        exit(0);

    }
    else if(0 == pid)
    {
         //子进程读管道,不写.所以关闭管道的写段.
        close(fd[1]);
        char buf[50]={0};
        // 读阻塞. 父进程会等待一会,在写管道 .
        read(fd[0],buf,sizeof(buf)); 
        printf("pipe %s\n",buf);
        exit(0);
    }
    else  
    {
        perror("fork");
        exit(1);
    }

    //system("pause");
    return 0;
}

3.2 无名管道

3.2.1 无名管道特性

  1. 亲缘关系进程使用
  2. 有固定的读写端

3.2.2 无名管道使用流程

创建并打开管道:pipe函数

#include <unistd.h>
int pipe(int pipefd[2]);

功能:创建并打开一个无名管道
参数:

  • pipefd[0]:无名管道的固定读端
  • pipefd[1]:无名管道的固定写端
    返回值:成功0,失败-1

注意事项:

  1. 无名管道的架设应该在fork之前进行。

无名管道的读写:使用文件IO的读写方式

  • 读:read()
  • 写:write()

关闭管道:close()

管道通信举例代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char **argv)
{
  int fd[2] = {0};
  int ret = pipe(fd);
  pid_t pid = fork();
  if (pid > 0)
  {
    //父进程写管道,不读.所以关闭管道的读段.
    close(fd[0]);
    int i = 0;
    char buf[1024] = {0};
    memset(buf, 'a', sizeof(buf));
    //这个地方会写阻塞, 管道大小64K 
    for (i = 0; i < 65; ++i)
    {
      write(fd[1], buf, sizeof(buf));
      printf("i  is %d\n", i + 1);
    }
  }
  else if (0 == pid)
  {
    //子进程读管道,不写.所以关闭管道的写段.
    close(fd[1]);
    int i = 5;
    while (i--)
    {
      sleep(1);
    }
  }
  else
  {
    perror("fork");
    exit(1);
  }

  // system("pause");
  return 0;
}

管道破裂的代码举例如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char **argv)
{
  int fd[2] = {0};
  int ret = pipe(fd);
  pid_t pid = fork();
  if (pid > 0)
  {
    //父进程写管道,不读.所以关闭管道的读段.
    close(fd[0]);
    char buf[] = "hello";
    printf("father id:%d\n", getpid());
    sleep(5);  //确保子进程 关闭管道的读段
    //借助gdb 观测管道破裂
    write(fd[1], buf, strlen(buf));  // 管道破裂 ,当前进程结束,异常
    printf("------------\n");
  }
  else if (0 == pid)
  {
    printf("child %d\n", getpid());
    //子进程读管道,不写.所以关闭管道的写段.
    close(fd[1]);
    close(fd[0]);  //读段关闭
    int i = 5;
  }
  else
  {
    perror("fork");
    exit(1);
  }

  // system("pause");
  return 0;
}

下面代码为无名管道的单相数据传递,最基础的

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char **argv)
{
  int fd[2] = {0};
  int ret = pipe(fd);
  pid_t pid = fork();
  if (pid > 0)
  {
    //父进程写管道,不读.所以关闭管道的读段.
    close(fd[0]);
    char buf[] = "hello";
    write(fd[1], buf, strlen(buf));
    exit(0);
  }
  else if (0 == pid)
  {
    //子进程读管道,不写.所以关闭管道的写段.
    close(fd[1]);
    while (1)
    {
      char buf[50] = {0};
      int ret = read(fd[0], buf, sizeof(buf));
      // read的返回值,如果为0 代表通信结束
      if (ret <= 0)
      {
        printf("通讯结束\n");
        break;
      }
      printf("pipe %s\n", buf);
    }

    exit(0);
  }
  else
  {
    perror("fork");
    exit(1);
  }

  // system("pause");
  return 0;
}

复制一个二进制文件,然后读取,在父子进程中:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <time.h>
int	main(int argc, char **argv)
{
    int fd[2]={0};
    int ret = pipe(fd);
    pid_t pid = fork();
    if(pid>0)
    {
        close(fd[0]);
      int srcfd = open("/home/linux/1.png",O_RDONLY);
      if(-1 == srcfd )
      {
        perror("father open");
        exit(1);
      }
      int num =0;
      while(1)
      {
        char buf[4096]={0};
        int ret = read(srcfd,buf,sizeof(buf));
        if(ret<=0)
        {
            time_t tm;
            time(&tm);
            printf("发送结束,num %d, %lu\n",num,tm);
            break;
        }
        write(fd[1],buf,ret);
        num+=ret;
      }
        close(srcfd);
        close(fd[1]);
        exit(0);
    }
    else if(0 == pid)
    {
        close(fd[1]);
      
        int dstfd = open("2.png",O_WRONLY|O_CREAT|O_TRUNC,0666);
        if(-1 == dstfd )
      {
        perror("child open");
        exit(1);
      }
        int num = 0 ;
      while(1)
      {
        char buf[4096]={0};
        int ret = read(fd[0],buf,sizeof(buf));
        if(ret<=0)
        {
            time_t tm;
            time(&tm);
            printf("读取数据结束,%d, tm:%lu\n",num,tm);
            break;
        }
        write(dstfd,buf,ret);
        num+=ret;
      }
      close(fd[0]);
      close(dstfd);
      exit(0);
    }
    else  
    {
        perror("fork");
        exit(1);
    }

3.2.3 无名管道练习验证

练习:
设计一个多进程程序,用无名管道完成父子进程间的任意信息传送,包括数字、字符串等。

验证问题:

  1. 父子进程是否都有fd[0] fd[1]?
    • 可以,写fd[1]可以从fd[0]读
  2. 管道的数据存储方式?
    • 队列形式存储,读数据会剪切取走数据不会保留,先进先出
  3. 管道的数据容量?
    • 操作系统建议值:512*8=4k
    • 实际测试值:65536byte=64k
  4. 管道的同步效果?
    • 读端关闭不能写(→SIGPIPE异常终止)
    • 写端关闭可以读(取决于pipe有无内容,read返回0不阻塞)
  5. 固定的读写端能否互换?能否写fd[0]能否读fd[1]?
    • 不可以,是固定读写端

双向通信实现方案:

pipe();
fork();

if(pid>0) {
    read(file,);
    write(fd[1]);
}

if(0 == pid) {
    read(fd[0]);
    write(newfile);
}

3.3 有名管道

有名管道===》fifo==》有文件名称的管道

3.3.1 有名管道框架

创建有名管道 → 打开有名管道 → 读写管道 → 关闭管道 → 卸载有名管道

3.3.2 有名管道操作函数

  1. 创建:mkfifo
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

功能:创建权限为mode的有名管道文件
参数:

  • pathname:路径+名称
  • mode:8进制文件权限
    返回值:成功0,失败-1
  1. 打开:open
    注意事项:
  • 打开方式决定当前进程的读写方式
  • 只能是O_RDONLY(固定读端)或O_WRONLY(固定写端)
  • 不能使用O_RDWR或O_CREAT
  1. 读写:使用文件IO
  • 读:read(fd-read,buff,sizeof(buff))
  • 写:write(fd-write,buff,sizeof(buff))
  1. 关闭:close(fd)

  2. 卸载:unlink

int unlink(const char *pathname);

功能:卸载并删除有名管道文件
参数: ptahtname 要卸载的有名管道
返回值:成功0,失败-1

有名管道举例代码:
读操作代码:

#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<time.h>
#include<unistd.h>
#include<errno.h>
int	main(int argc, char **argv)
{
    int ret = mkfifo("myfifo",0666);
    if(-1 == ret)
    {
        perror("mkfifo");
        exit(1);
        }
        int fd =open("mkfifo",O_WRONLY);
        if(-1 == fd)
        {
            perror("open");
            exit(1);
        }
        char buf[50] = {0};
        read(fd,buf,sizeof(buf));
        printf("fifo:%s\n",buf);
        close(fd);
        //remove("myfifo");





    //system("pause");
    return 0;
}

写操作代码:

#include <asm-generic/errno-base.h>
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<time.h>
#include<unistd.h>
#include<errno.h>
int	main(int argc, char **argv)
{
    int ret = mkfifo("myfifo",0666);
    if(-1 == ret)
    {
        if(EEXIST!=errno)
        {
        perror("mkfifo");
        exit(1);
        }
    }
        int fd =open("mkfifo",O_WRONLY);
        if(-1 == fd)
        {
            perror("open");
            exit(1);
        }
        char buf[50] ="hello";
        write(fd,buf,strlen(buf));
        //printf("fifo:%s\n",buf);
        close(fd);
        //remove("myfifo");





    //system("pause");
    return 0;
}

3.3.3 有名管道注意事项

  1. 同步问题:
    • 必须有读写端同时存在
    • 如果有一端没有打开,open函数会阻塞
  2. 亲缘关系进程使用:
    • 可以在有亲缘关系的进程间使用
    • 注意启动次序可能导致阻塞
  3. 手工操作:
    • 读:cat fifoname
    • 写:echo “content” > fifoname
      1)明确点:是否需要同步?同步的位置
      读写端关闭,是否可以写,不能写的话,是什么原因,写端关闭,是否可以读。
      2)结论:有名管道执行过程中必须读写端同时存在,如果有一段没有打开,则默认在open函数部分阻塞
      3)、能否手工操作有名管道实现数据的传送。
      读: cat fifoname
      写: echo “asdfasdf” > fifoname
      strcat
      举例项目代码:可以实现信息的传递:
#include <errno.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<time.h>
#include<unistd.h>
#include<errno.h>
#include<fcntl.h>
int	main(int argc, char **argv)
{
    int ret = mkfifo("myfifo",0666);
    if(-1 == ret)
    {
        if(EEXIST != errno)
        {
            perror("mkfifo");
            exit(1);

        }
    }
    int fd = open("myfifo",O_WRONLY);
    if(-1==fd)
    {
        perror("open");
        exit(1);
    }
    int srcfd =open("/honme/linux/1.png",O_RDONLY);
    if(-1==srcfd){
        perror("open srcfd");
        exit(1);
    }
    while(1)
    {
        char buf[4096] = {0};
        int ret = read(srcfd,buf,sizeof(buf));
        if(ret<= 0)
        {
            break;
        }
        write(fd,buf,ret);
    }
    close(fd);
    close(srcfd);
    //system("pause");
    return 0;
}

读操作:

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
int main(int argc, char **argv)
{
  int ret = mkfifo("myfifo", 0666);
  if (-1 == ret)
  {
    //如果错误的原因是 不是fifo 文件已经存在,
    if (EEXIST != errno)
    {
      perror("mkfifo");
      exit(1);
    }
  }
  int fd = open("myfifo", O_RDONLY);
  if (-1 == fd)
  {
    perror("open");
    exit(1);
  }
  int dstfd = open("2.png", O_WRONLY | O_CREAT | O_TRUNC, 0666);
  if (-1 == dstfd)
  {
    perror("open dstfd");
    exit(1);
  }
  while (1)
  {
    char buf[1024] = {0};
    int ret = read(fd, buf, sizeof(buf));
    if(ret<=0)
    {
      break;
    }
    write(dstfd,buf,ret);
  }

  close(fd);
  close(dstfd);
  // remove("myfifo");
  // system("pause");
  return 0;
}

4. 信号通信

进程间通信 → 信号通信 signal
应用:异步通信、中断
信号范围:1~64(32用于应用编程)

4.1 信号发送端

  1. kill函数
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

功能:给pid进程发送sig信号
参数:

  • pid:接收进程pid
  • sig:信号编号(kill -l查看)
    返回值:成功0,失败-1
  1. raise函数
int raise(int sig);

等价于kill(getpid(),int sig)

  1. alarm函数
unsigned int alarm(unsigned int seconds);

功能:定时发送SIGALRM信号

  1. pause函数
int pause(void);

功能:进程暂停,直到收到信号

4.2 信号接收处理

4.2.1 信号处理方式

  1. 默认处理
  2. 忽略处理(9,19信号不能忽略)
  3. 自定义处理(9,19信号不能自定义)

4.2.2 信号注册函数

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数:

  • handler可以是:
    • SIG_DFL:默认处理
    • SIG_IGN:忽略处理
    • 自定义函数:void fun(int sig)

特殊信号:

  • 10 SIGUSR1
  • 12 SIGUSR2
    (预留给程序员使用的未定义信号)

5. 练习与作业

5.1 基础练习

  1. 编写信号处理函数,对SIGUSR1和SIGUSR2输出不同语句
  2. 验证信号反复注册的处理流程(最后注册的有效)

5.2 大作业

  1. 修改有名管道通信程序,添加信号处理:

    • 发送quit时,进程A发送10或12信号
    • 进程B收到信号后退出
  2. 创建多进程程序处理信号:

    • 子进程:
      • 收到10信号打印a.txt
      • 收到12信号打印b.txt
      • 收到13信号退出
    • 父进程:
      • 提示用户输入
      • 根据输入决定发送信号编号

网站公告

今日签到

点亮在社区的每一天
去签到