6 Linux 进程间通信(IPC)


前言 (含目录)


/**
 * @author IYATT-yx
 * @brief 验证进程之间无法使用全局变量进行通信
 */
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int num = 15;

int main(void)
{
    
    
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        printf("父: %d\n", num);
        ++num;
        printf("父: %d\n", num);
    }
    else if (pid == 0)
    {
    
    
        printf("子: %d\n", num);
        --num;
        printf("子: %d\n", num);
    }
    else
    {
    
    
        perror("fork");
    }
    
}

运行结果:
在这里插入图片描述

/**
 * @author IYATT-yx
 * @brief 进程间使用文件进行通信
 */
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string.h>

int main(void)
{
    
    
    int fdRW = open("/tmp/temp", O_CREAT | O_RDWR, 0664);
    if (fdRW == -1)
    {
    
    
        perror("open /tmp/temp");
        return -1;
    }

    // 子进程的虚拟地址空间和父进程基本一样 (除pid)
    // 可以使用同一个文件描述符,且都指向相同的文件
    pid_t pid = fork();
    if (pid == 0)
    {
    
    
        const char *str = "验证使用文件在进程间通信";
        write(fdRW, str, strlen(str));
        close(fdRW);
    }
    else if (pid > 0)
    {
    
    
        wait(NULL);

        char buf[1024];
        memset(buf, 0, 1024);
        lseek(fdRW, 0, SEEK_SET);
        read(fdRW, buf, sizeof(buf));
        printf("%s\n", buf);

        remove("/tmp/temp");
    }
}



IPC常用4种方式

  • 管道 (简单)
  • 内存映射 (通信的进程之间可以没有"血缘"关系)
  • 信号 (麻烦,但是信号属于操作系统的功能, 资源开销小)
  • 本地套接字 (稳定, 见后面网络编程部分)

pipe(匿名管道)
本质 ①内核缓冲区
②不占用磁盘空间,伪文件,但是可以当作文件读写
特点 ①两部分: 读端和写端各对应一个文件描述符,写端流入,读端流出
②操作管道的进程被
销毁后,自动销毁管道
③管道默认阻塞
原理 内部实现方式: 队列
* 环形队列
* 先进先出
缓冲区大小:
* 默认4K
* 大小根据实际情况适当调整
局限性 队列:
* 数据只能读取一次
半双工:
* 数据可以在一个信号载体的两个方向上传输,但是不能同时传输.
匿名管道:
* 有"血缘"关系的进程之间通信
/**
 * @author IYATT-yx
 * @brief 借助管道实现 ps ajx | grep bash , 父子进程通信 
 */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(void)
{
    
    
    // 创建管道
    // 下标0为读,下标1为写
    int fdPipe[2];
    if (pipe(fdPipe) == -1)
    {
    
    
        perror("pipe");
        return -1;
    }
    
    pid_t pid = fork();
    // 子进程执行 ps ajx
    // 执行结果默认是写到 STDOUT_FILENO
    // 此处需要重定向到 fdPipe[1], 写入管道
    if (pid == 0)
    {
    
    
        // 写时,关读
        close(fdPipe[0]);
        // 重定向文件描述符,写到终端改为写到管道
        dup2(fdPipe[1], STDOUT_FILENO);

        execlp("ps", "", "ajx", NULL);
        perror("execlp ps");
        return -1;
    }
    // 父进程执行 grep bash
    // 获取查找来源默认是从 STDIN_FILENO
    // 此处需要重定向到 fdPipe[0], 从管道读取
    else if (pid > 0)
    {
    
    
        // 读时,关写
        close(fdPipe[1]);
        // 重定向文件描述符,从终端读取改为从管道读取
        dup2(fdPipe[0], STDIN_FILENO);
        
        execlp("grep", "", "--color=auto", "bash", NULL);
        perror("execlp grep");
        return -1;
    }
}

在这里插入图片描述

/**
 * @author IYATT-yx
 * @brief 借助管道实现 ps ajx | grep bash, 兄弟进程通信
 */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void)
{
    
    
    // 创建管道
    // 下标0为读,下标1为写
    int fdPipe[2];
    if (pipe(fdPipe) == -1)
    {
    
    
        perror("pipe");
        return -1;
    }
    
    int i;
    for (i = 0; i < 2; ++i)
    {
    
    
        pid_t pid = fork();
        if (pid == -1)
        {
    
    
            perror("fork");
            return -1;
        }
        // 防止子进程继续创建子进程
        else if (pid == 0)
        {
    
    
            break;
        }
    }

    // 子进程1: ps ajx
    if (i == 0)
    {
    
    
        close(fdPipe[0]);
        dup2(fdPipe[1], STDOUT_FILENO);
        execlp("ps", "", "ajx", NULL);
        perror("execlp ps");
        return -1;
    }
    // 子进程2: grep bash
    else if (i == 1)
    {
    
    
        close(fdPipe[1]);
        dup2(fdPipe[0], STDIN_FILENO);
        execlp("grep", "", "--color=auto", "bash", NULL);
        perror("execlp grep");
        return -1;
    }
    // 父进程: 等待回收子进程
    else if (i == 2)
    {
    
    
        close(fdPipe[0]);
        close(fdPipe[1]);
        while (wait(NULL) != -1)
        {
    
    
            ;
        }
    }
}
管道的读写
有数据:
* read()正常读取,返回读取的字节数

无数据:
* 写端全部关闭:
* * * * read()解除阻塞,返回0,相当于读取到文件的尾部
* 没有全部关闭:
* * * * read()阻塞
读端全部被关闭:
* 管道破裂,进程被终止,内核给当前进程发 SIGPIPE 信号

读端没全部关闭:
* 缓冲区写满了,write阻塞
* 缓冲区没写满,write继续写
# 查看pipe大小
ulimit -a

在这里插入图片描述
512bytes x 8 = 4096byte = 4KB

也可以借助fpathconf函数查看各种属性信息,具体可使用man命令获取更多帮助.

/**
 * @author IYATT-yx
 * @brief 借助 fpathconf 查看 pipe 缓冲区大小
 */
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    
    
    // 创建管道
    // 下标0为读,下标1为写
    int fdPipe[2];
    if (pipe(fdPipe) == -1)
    {
    
    
        perror("pipe");
        return -1;
    }

    long size = fpathconf(fdPipe[0], _PC_PIPE_BUF);
    size >>= 10;
    printf("pipe缓冲区大小: %ldKB\n", size);
}

在这里插入图片描述

pipe默认读写都是阻塞,可以使用前面文件属性部分提到的fcntl函数修改为非阻塞.

// fd 为管道
// 设置读端为非阻塞,写端同理

// 获取文件描述符原open时的属性
int flag = fcntl(fd[0], F_GETFL);
// 添加非阻塞属性
flag |= O_NONBLOCK;
fcntl(fd[0], F_SETFL, flag);
fifo (有名管道)
特点 在磁盘上存在一个文件,大小为0,伪文件,实际数据依然在内核中. 半双工.
使用场景 没有"血缘"关系的进程之间通信
创建方式 命令: mkfifo
函数: mkfifo()

fifo使用和读写普通文件几乎一样,先open(),然后read()和write(),只是不能用open来创建,可以用mkfifo函数或命令来创建.


mmap (内存映射)

  • 作用: 将磁盘文件的数据映射到内存,用户通过修改内存来修改磁盘文件,借助mmap也可以实现"非血缘"关系的进程间的通信.
// 创建内存映射区
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数 说明
addr 映射区的首地址, 传 NULL
length 映射区的大小
一般指定为文件的大小,但是内部实际是:文件不足4K为4K,超过4K为可以容纳文件大小的最小的4K的整数倍
prot 映射区权限:
PROT_READ 读
PROT_WRITE 写

使用时至少要有读权限
flags 标志位参数:
MAP_SHARED 修改内存数据会同步到磁盘
MAP_PRIVATE 修改了内存数据不会同步到磁盘

MAP_ANONYMOUS 或 MAP_ANON 匿名
fd 要映射的文件对应的文件描述符
offset 映射文件的偏移量
* 偏移量必需是4K的整数倍*
* 一般不偏移设为 0
返回值 成功: 映射区的首地址
失败: 返回 MAP_FAILED
// 释放内存映射区
int munmap(void *addr, size_t length);
参数 说明
addr mmap的返回值, 映射区的首地址
length mmap的参数length, 映射区的长度
/**
 * @author IYATT-yx
 * @brief 使用内存映射读取磁盘文件 /etc/apt/sources.list
 */
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

int main(void)
{
    
    
    int fdR = open("/etc/apt/sources.list", O_RDONLY);
    if (fdR == -1)
    {
    
    
        perror("open");
        return -1;
    }
    // 获取文件内容长度
    size_t len = (size_t)lseek(fdR,0, SEEK_END);
    // 创建内存映射区
    void *ptr = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fdR, 0);
    // 显示内存映射区的内容
    printf("%s\n", (char *)ptr);
    // 释放内存映射区
    if (munmap(ptr, len) == -1)
    {
    
    
        perror("munmap");
    }
}
/**
 * @author IYATT-yx
 * @brief 使用内存映射进行进程间通信 (有名)
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/mman.h> 
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/wait.h>

int main(void)
{
    
    
    int fd = open("/tmp/mmapTemp", O_RDWR | O_CREAT, 0664);
    if (fd == -1)
    {
    
    
        perror("open mmapTemp");
        return -1;
    }
    ftruncate(fd, 4096);
    size_t len = (size_t)lseek(fd, 0, SEEK_END);
    void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED)
    {
    
    
        perror("mmap");
        return -1;
    }

    pid_t pid = fork();
    if (pid == 0)
    {
    
    
        strcpy(ptr, "测试mmap进程间通信");
    }
    else if (pid > 0)
    {
    
    
        wait(NULL);
        printf("%s\n", (char *)ptr);
    }

    munmap(ptr, len);
    close(fd);
}
/**
 * @author IYATT-yx
 * @brief 使用内存映射进行进程间通信 (匿名), 不借助文件
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main(void)
{
    
    
    size_t len = 4096;
    void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
    if (ptr == MAP_FAILED)
    {
    
    
        perror("mmap");
        return -1;
    }

    pid_t pid = fork();
    if (pid == 0)
    {
    
    
        strcpy(ptr, "测试mmap进程间通信");
    }
    else if (pid > 0)
    {
    
    
        wait(NULL);
        printf("%s\n", (char *)ptr);
    }

    munmap(ptr, len);
}

在"非血缘"关系的进程间通信时,不能使用匿名映射区. 需要创建文件, 分别进行内存映射操作.


信号

特点 简单
携带信息量少
使用在某个特定的场景中
信号的状态 产生
未决: 没有被处理的
递达: 信号被处理了
产生 键盘: Ctrl + C 终止进程
命令: kill
系统函数: kill()
软条件: 定时器
硬件: 段错误, 除0错误

信号的优先级高,进程收到信号后,会暂停正在执行的任务,处理完信号后,再继续原来的工作

查询信号相关的文档man 7 signal, 查看信号宏kill -l

// 向指定pid的进程发送信号
int kill(pid_t pid, int sig);
// 向进程自己发送信号
int raise(int sig);
// 向自己发送 SIGABRT 信号 (异常终止)
void abort(void);
/**
 * @author IYATT-yx
 * @brief raise 向自己发信号
 */
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    
    
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        int sig;
        wait(&sig);
        if (WIFSIGNALED(sig))
        {
    
    
            printf("子进程被信号 %d 终止\n", WTERMSIG(sig));
        }
    }
    else if (pid == 0)
    {
    
    
        raise(SIGINT);
    }
}
// 定时发送信号 SIGALRM
unsigned int alarm(unsigned int seconds);

在这里插入图片描述

/**
 * @author IYATT-yx
 * @brief alarm 定时发送信号终止进程
 */
#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>

int main(void)
{
    
    
    alarm(6);
    sleep(2);
    // 重新设定定时器
    printf("上一个定时器还剩 %d 秒\n", alarm(3));

    while (true)
    {
    
    
        printf("循环\n");
        sleep(1);
    }
}
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

struct itimerval
{
    
    
	// 周期
	struct timeval it_interval;
	// 首次
	struct timeval it_value;
};

struct timeval
{
    
    
	// 秒
	time_t      tv_sec;
	// 微秒
	suseconds_t tv_usec;
};

代码应用示例可见下一节 7 Linux 守护进程

信号集
未决信号集:

  • 没有被当前进程处理的信号
  • 收到的信号首先放进未决信号集,然后判断阻塞信号集中该信号对应的标志位是否为 1 , 如果为 1 则不处理,如果为 0 则处理.

阻塞信号集:

  • 将某个信号放到阻塞信号集,则这个信号就不会被进程处理
  • 阻塞解除后,信号被处理
// 将set集合置空
int sigemptyset(sigset_t *set);
// 将所有信号加入set集合
int sigfillset(sigset_t *set);
// 将signo信号加入到set集合
int sigaddset(sigset_t *set, int signum);
// 从set集合中移除signo信号
int sigdelset(sigset_t *set, int signum);
// 判断信号是否存在
int sigismember(const sigset_t *set, int signum);

// 屏蔽and接触信号,将自定义信号集设置给阻塞信号集
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
// 内核将未决信号集写入set
int sigpending(sigset_t *set);
/**
 * @author IYATT-yx
 * @brief 设置阻塞信号
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <stdbool.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
    
    
    sigset_t sigSet;
    // 置空
    sigemptyset(&sigSet);
    // 阻塞 Ctrl + C
    sigaddset(&sigSet, SIGINT);
    // 阻塞 Ctrl + '\'
    sigaddset(&sigSet, SIGQUIT);
    // 试验看能否阻塞 SIGKILL 信号
    sigaddset(&sigSet, SIGKILL);

    sigprocmask(SIG_BLOCK, &sigSet, NULL);

    pid_t pid = getpid();
    while (true)
    {
    
    
        // 输出自己的pid, 方便测试 kill -SIGKILL
        printf("pid = %d\n", pid);

        // 获取未决信号集
        sigset_t pend;
        sigpending(&pend);
        // 输出前31号,1为未决
        for (int i = 1; i < 32; ++i)
        {
    
    
            printf("%d", sigismember(&pend, i));
        }
        sleep(1);
        printf("\n");
    }
}

在这里插入图片描述
这里也验证了, SIGKILL信号是无条件终结进程的, 不可阻塞

/**
 * @author IYATT-yx
 * @brief 捕捉信号
 */
#include <stdio.h>
#include <signal.h>
#include <stdbool.h>
#include <unistd.h>

void info(int no)
{
    
    
    if (no == SIGINT)
    {
    
    
        printf("捕捉到信号 SIGKILL\n");
    }
}

int main(void)
{
    
    
	// 第二个参数为回调函数
    signal(SIGINT, info);
    while (true)
    {
    
    
        printf("检测 Ctrl + C 中...\n");
        sleep(1);
    }
}

在这里插入图片描述
可以将这里的捕捉信号改为 SIGKILL , 也可以验证这个信号不可捕捉,程序会被杀死.
即不可阻塞,不可捕捉,任何进程收到 SIGKILL 都会被无条件的杀死.

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

// sa_flags 为 0 ,则使用第一个
// sa_flags 为 1 ,则使用第二个
struct sigaction
{
    
    
	void     (*sa_handler)(int);
	void     (*sa_sigaction)(int, siginfo_t *, void *);
	sigset_t   sa_mask;
	int        sa_flags;
	void     (*sa_restorer)(void);
};
/**
 * @author IYATT-yx
 * @brief sigaction 捕捉信号
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <stdbool.h>
#include <unistd.h>

void info(int no)
{
    
    
    if (no == SIGINT)
    {
    
    
        printf("捕捉到信号 SIGKILL\n");
    }
}

int main(void)
{
    
    
    struct sigaction act;
    act.sa_flags = 0;
    // 可以对 act.sa_mask 设置实现临时屏蔽某个信号,待回调函数执行完之后再处理这个信号,这里未示例,直接清空
    sigaddset(&act.sa_mask, SIGQUIT);
    act.sa_handler = info;
    sigaction(SIGINT, &act, NULL);
    while (true)
    {
    
    
        sleep(1);
    }
}

signal只有第一次捕捉到设定的信号执行回调函数,之后按默认处理.
sigaction每次捕捉到设定的信号都是执行回调函数

猜你喜欢

转载自blog.csdn.net/weixin_45579994/article/details/112801551