Linux进程间通信

Linux中常用的通信方式有:

  • 信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  • 无名管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常指父子进程的关系。
  • 命名管道FIFO:命名管道也是半双工的通信方式,但是它允许无亲缘关系的进程间的通信。
  • 消息队列(message queue):消息队列是由消息的链表存放在内核中,并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限的问题。
  • 共享内存:就是映射一段能被其他进程所访问的内存,这段内存由一个进程创建,但是多个进程都能访问。是最快的IPC方式。往往与其他通信机制进行配合使用(如信号量),来实现进程间的同步和通信。
  • 信号量:是一个计数器,控制进程对共享资源的访问,主要作为进程间的同步手段。
  • 套接字(socket):可以用于不同机器间的进程间通信。

信号

信号可以在任何时候发送给某一进程,而无需知道进程的状态。如果进程并未处于执行状态,则由内核保存起来,待进程恢复执行并传递给它为止。
如果要使用linux提供的信号,通常需要包含<signal.h>头文件
使用系统命令 kill -l 可以查看系统提供的所有信号类型

操作

信号产生后用户进程有如下三种操作:

  • 执行默认操作。
  • 捕捉信号。自定义信号处理函数,信号产生时执行相应的处理函数。
  • 忽略信号。不对信号做任何处理。
    其中SIGKILL和SIGSTOP是无法捕捉和忽略的。

常见的信号及默认操作

信号名含义默认操作
SIGINT用户按下Ctrl+C时系统会向终端相应的进程发送该信号终止
SIGQUIT用户按下Ctrl+\时系统会向终端相应的进程发送该信号终止
SIGILL当一个进程企图执行非法指令时发出(比如堆栈溢出)终止
SIGFPE在发生致命的算术错误运算时发出终止
SIGALRM在定时器计时完成时发出,定时器可由进程调用alarm函数来设置
SIGTSTP用户按下Ctrl+Z时系统会向终端相应的进程发送该信号
SIGCHLD在子进程结束时向父进程发出。只有在父进程运行wait函数时才会被捕捉。

函数

函数名kill
头文件#include<sys/types.h>
#include<signal.h>
功能发送信号给指定的进程
原型int kill(pid_t pid, int sig)
传入值说明参数pid的几种情况:
pid>0 或 pid<-1,将信号发送给进程识别码为pid绝对值的进程
pid=0,将信号发送给和目前进程相同进程组的所有进程
pid=-1,将信号广播给所有的进程
函数返回值执行成功返回0,如果有错误返回-1
函数名raise
头文件#include<signal.h>
功能给当前进程发送信号
原型int raise(int sig)
函数返回值执行成功返回0,如果有错误返回-1
函数名signal
头文件#include<signal.h>
功能设置信号处理方式
原型void(*signal(int signum, void(*handler)(int)))(int);
传入值说明signum是指定的信号类型
handler是自定义的处理函数
如果handler不是函数指针,则必须是以下两个常数之一:
SIG_IGN:忽略信号
SIG_DFL:恢复默认处理方式
函数返回值返回先前的信号处理函数指针,有错误则返回SIG_ERR(-1)

示例程序

实现用户按下两次ctrl+z才会退出

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

void fun_ctrl_z2()
{
    printf("你第二次按下了ctrl+z,程序将退出\n");
    exit(0);
}

void fun_ctrl_z1()
{
    printf("你第一次按下了ctrl+z,如果再次按下程序将会退出\n");
    signal(SIGTSTP, fun_ctrl_z2);
}

int main()
{
    signal(SIGTSTP, fun_ctrl_z1);
    while (1)
    {
        printf("程序正在运行...\n");
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

无名管道

管道主要用于父子进程间的通信。通过pipe()系统调用创建并打开无名管道,当最后一个使用它的进程关闭对它的引用时,将自动撤销。

操作

  • 父进程调用pipe函数开辟管道,得到两个文件描述符指向管道的两端
  • 父进程调用fork函数创建子进程,子进程也会得到两个文件描述符指向管道的两端
  • 进程通过文件描述符向管道中写入或读取数据。父子进程都可以读写管道,但是为了同步,一般只允许一个进程读,以及另一个进程写。

函数

函数名pipe
头文件#include<unistd.h>
功能建立管道
函数原型int pipe(int filedes[2]);
传入值说明执行后filedes[0]是读取端的文件描述符,filedes[1]是写入端的文件描述符
返回值成功返回0,错误返回-1

示例程序

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

int main()
{
    int fd[2];
    pipe(fd); //创建管道
    int cnt, res = fork();
    char buf[105];

    if (res == 0) //子进程
    {
        memset(buf, 0, sizeof(buf));
        while (1)
        {
            cnt = read(fd[0], buf, 100); //当管道中没有内容的时候read会返回0
            if (cnt == 0) //如果没有读取到内容就继续循环(说明父进程还没写入)
            {
                sleep(1);
                continue;
            }
            else if (cnt == -1)
                perror("读取管道内容失败!\n");
            printf("子进程读取了%d个字节,内容为:\n%s\n", cnt, buf);
            break;
        }
        close(fd[0]), close(fd[1]); //及时关闭文件流
    }
    else //父进程
    {
        memset(buf, 0, sizeof(buf));
        for (int i = 0; i < 100; i++)
            buf[i] = 'a' + i % 26;
        cnt = write(fd[1], buf, 100);
        if (cnt != -1)
            printf("父进程写入了%d个字节,内容为:\n%s\n", cnt, buf);
        else
            perror("写入管道失败!\n");
        close(fd[0]), close(fd[1]); //及时关闭文件流
    }
    return 0;
}

运行结果:

父进程写入了100个字节,内容为:
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv
子进程读取了100个字节,内容为:
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv

命名管道

和无名管道的工作机制很类似,只是命名管道有名字,相当于在磁盘上创建了一个中介文件用于几个进程间的通信。因此在管道的创建、打开、删除上和无名管道有一定的区别,和普通文件的操作更加类似。当一个命名管道不再被任何进程打开时,它并没有消失,还可以被再次打开。可以像普通文件一样将其删除。如果删除的时候还有其他进程正在使用该管道,则会等到 89所有的进程都结束后再删除管道。
要注意的是, 当一个进程用open以只读方式打开命名管道时,会一直阻塞到另一个进程以只写方式打开该命名管道,反之亦然。

操作

  • 调用函数mkfifo建立一个命名管道
  • 根据读写方式用open函数打开这个命名管道
  • 应用宏建立文件描述符集合,设定等待时间,使用函数select实现非阻塞传送
  • 使用read、write读写管道
  • 读写完成关闭管道

函数

函数名select
头文件#include<sys/select.h>
功能用于监视一些文件描述符的变化情况——读写或异常
函数原型int select(int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout)
传入值说明int maxfdp:集合中所有文件描述符的范围,即所有描述符的最大值+1
fd_set *readfds:可读文件描述符集合
fd_set *writefds:可写文件描述符集合
fd_set *errorfds:错误异常文件描述符集合
struct timeval *timeout:超时时间,超出timeout的时间select将返回0
返回值没有可操作的文件或超出timeout时间返回0,存在可操作的文件返回正值,发生错误返回负值
备注struct fd_set是文件描述符的集合,可以通过以下宏进行操作:
FD_ZERO(fd_set *):清空集合
FD_SET(int, fd_set *):将一个给定的文件描述符加入到集合中
FD_CLR(int, fd_set *):将一个给定的文件描述符从集合中删除
FD_ISSET(int, fd_set *):检查集合中给定的文件描述符是否可读写

struct timeval *timeout若传入NULL,则select将一直阻塞直到出现可操作的文件描述符为止
函数名mkfifo
头文件#include<sys/types.h>
#include<sys/stat.h>
功能建立命名管道
函数原型int mkfifo(const char *pathname, mode_t mode)
传入值说明根据参数pathname建立特殊的FIFO文件,该文件必须不存在,而参数mode为该文件的权限,因此umask的值也会影响到FIFO文件的权限,用mkfifo创建的FIFO文件,其他进程都可以用读写一般文件的方式进行存取。
返回值成功返回0,失败返回-1

示例程序

由于使用到select函数的场景比较复杂,这里只介绍简单的命名管道应用
以下程序实现了一个进程读取文件写入命名管道,另一个进程从命名管道内读取内容并写入到copy文件里

//进程A
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
int main()
{
    char buf[105];
    int fd1, fd2, cnt;
    mkfifo("/tmp/fifotest5", 0666);
    fd1 = open("./test.txt", O_RDONLY);
    if (fd1 == -1)
    {
        perror("打开test.txt文件失败\n");
        return 1;
    }
    fd2 = open("/tmp/fifotest5", O_WRONLY);
    if (fd2 == -1)
    {
        perror("打开命名管道失败\n");
        return 1;
    }
    while (1)
    {
        cnt = read(fd1, buf, 100);
        if (cnt <= 0)
            break;
        buf[cnt] = 0;
        printf("进程A向管道内写入了:%s\n", buf);
        write(fd2, buf, cnt);
    }
    printf("进程A写入管道完成\n");
    close(fd1);
    close(fd2);
    unlink("/tmp/fifotest5");
    return 0;
}
//进程B
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
int main()
{
    char buf[105];
    int fd1, fd2, cnt;
    fd1 = open("./test_copy.txt", O_WRONLY | O_CREAT);
    if (fd1 == -1)
    {
        perror("打开test_copy.txt文件失败\n");
        return 1;
    }
    fd2 = open("/tmp/fifotest5", O_RDONLY);
    if (fd2 == -1)
    {
        perror("打开命名管道失败\n");
        return 1;
    }
    while (1)
    {
        cnt = read(fd2, buf, 100);
        if (cnt <= 0)
            break;
        write(fd1, buf, cnt);
    }
    printf("进程B读取管道完成\n");
    close(fd1);
    close(fd2);
    unlink("/tmp/fifotest5");
    return 0;
}

高级管道操作

popen函数可以用创建管道的方式启动一个进程并调用shell来执行command命令。
优点:可以完成复杂的shell命令。并且是C函数库的,支持更多的读写方式。
缺点:对于每个popen调用,不仅要启动一个被请求的程序,还要启动一个shell,比正常方式要慢一点。
当调用popen的父进程退出时,子进程也会退出。

操作

调用popen函数fork一个子进程并得到一个文件描述符,对文件描述符进行读写实现和子进程的通信。

函数

函数名popen
头文件#include<stdio.h>
功能建立管道IO
函数原型FILE *popen(const char *command, const char *type)
传入值说明调用fork产生子进程,然后子进程调用/bin/sh -c来执行command命令。
参数type为"r"则文件指针连接到command的标准输出,参数type为"w"则文件指针连接到command的标准输入。
返回值成功返回管道的文件流指针,否则返回NULL

示例程序

以下程序的功能是父进程调用popen获取命令ls -l的执行结果并写入到无名管道,子进程从无名管道中读取结果并调用popen执行grep 7-5,将执行结果输出到屏幕。

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

int main()
{
    int fd[2];
    pipe(fd);
    int res = fork();
    FILE *fp;
    char buf[10000];
    memset(buf, 0, sizeof(buf));
    if (res == 0)
    {
        close(fd[1]);
        fp = popen("grep 7-5", "w");
        while (1)
        {
            int cnt = read(fd[0], buf, 10000);
            if (cnt > 0)
            {
                printf("子进程成功读取运行结果,以下为 \"grep 7-5\" 的运行结果:\n");
                fprintf(fp, "%s\n", buf);
                break;
            }
            sleep(1);
        }
    }
    else
    {
        close(fd[0]);
        fp = popen("ls -l", "r");
        int cnt = fread(buf, sizeof(char), 10000, fp);
        write(fd[1], buf, cnt);
        pclose(fp);
        printf("以下为父进程 \"ls -l\" 的运行结果:\n");
        printf("%s\n", buf);
    }
    return 0;
}

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

消息队列

消息队列的特点是非实时性,发送方不必等待接收方受到了消息就可以继续工作,接收方也不必等待发送方发送了消息就可以继续工作。

操作

  • 使用ftok函数得到一个编号key
  • 应用key值作为msgget函数的参数,产生一个消息队列
  • 进程通过msgsnd来向消息队列发送信息,也可以通过msgrcv读取。两种操作都有可能被中途打断导致操作失败。
  • msgctl可以删除消息队列

注意点:
消息队列的个数是有限的,如果到达了上限,msgget会调用失败,产生的错误代码提示为"no space left on device"
如果消息队列满了msgsnd会调用失败。

函数

函数名ftok
头文件#include<sys/types.h>
#include<sys/ipc.h>
功能获取一个用于建立IPC通讯的ID值
函数原型key_t ftok(char *pathname, char id)
传入值说明pathname:文件名含路径
id:子序号
返回值成功则返回key_t值,否则返回-1
函数名msgget
头文件#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
功能建立消息队列
函数原型int msgget(key_t key, int msgflg)
传入值说明参数msgflg用来决定消息队列的存取权限,取值如下:
IPC_CREAT:如果消息队列对象不存在则创建,否则进行打开操作
IPC_EXCL:如果消息对象不存在则创建,否则产生一个错误并返回,需要用 | 连接IPC_CREAT一起使用,不可单独使用
返回值成功返回消息队列的识别号,否则返回-1
函数名msgsnd
头文件#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
功能向消息队列中发送消息
函数原型int msgsnd(int msqid, struct msgbuf *msgp, int msgsz, int msgflg)
传入值说明msqid:消息队列的识别码
msgp:指向消息缓冲区的指针,此位置用于暂存发送和接收的消息,是一个用户可定义的通用结构,形态见下方代码
msgsz:消息的大小,用来指定消息数据的长度
msgflg:用来指明核心程序在队列没有数据的情况下所应采取的行动
返回值成功返回0,否则返回-1
struct msgbuf{		//struct msgbuf形态
	long mtype; 	//消息类型,必须大于0
	char mtext[]; 	//消息文本
};

msgflg说明:

  • 0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列
  • IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回
  • MSG_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。(某些内核版本可能无法使用)
函数名msgrcv
头文件#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
功能从消息队列中读取信息
函数原型int msgrcv(int msqid, struct msgbuf *msgp, int msgsz, long msgtyp, int msgflg)
传入值说明msqid:消息队列的识别码
msgp:指向消息缓冲区的指针,此位置用于暂存发送和接收的消息,是一个用户可定义的通用结构,形态见下方代码
msgsz:消息的大小,用来指定消息数据的长度
msgtyp:用来指定所要读取的信息种类。若等于0,返回队列内第一项消息;若大于0,返回队列内第一项类型相同的消息;若小于0,返回队列内第一项小于或等于该绝对值的消息
msgflg:用来指明核心程序在队列没有数据的情况下所应采取的行动
返回值成功返回实际读取的消息数据长度,否则返回-1

msgflg说明:

  • 0: 阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待
  • IPC_NOWAIT:如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG
  • MSG_EXCEPT:与msgtype配合使用返回队列中第一个类型不为msgtype的消息(某些内核版本可能无法使用)
  • MSG_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将被丢弃(某些内核版本可能无法使用)

示例程序

以下程序实现了多个进程向消息队列中写,一个进程从消息队列中读。
如果在运行时提示没有权限访问,则需要在root权限下运行。

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

struct msgbuf
{
    long msg_type;
    char msg_text[600];
};

int main()
{
    key_t key = ftok("/tmp/msgqueue6", 1);
    if (key == -1)
        perror("获取ID出错\n");

    int qid = msgget(key, IPC_CREAT);
    if (qid == -1)
        perror("打开消息队列出错\n");

    if (key != -1 && qid != -1)
    {
        struct msgbuf msg;
        msg.msg_type = getpid();
        memcpy(msg.msg_text, "123456", 6);
        if (msgsnd(qid, &msg, strlen(msg.msg_text), IPC_NOWAIT) == -1)
            perror("发送失败\n");
        else
            printf("发送成功\n");
    }
    return 0;
}
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

struct msgbuf
{
    long msg_type;
    char msg_text[501];
};

int main()
{
    key_t key = ftok("/tmp/msgqueue6", 1);
    if (key == -1)
        perror("获取ID出错\n");

    int qid = msgget(key, IPC_CREAT);
    if (qid == -1)
        perror("打开消息队列出错\n");

    if (key != -1 && qid != -1)
    {
        struct msgbuf msg;
        while (msgrcv(qid, &msg, 500, 0, IPC_NOWAIT) != -1)
        {
            printf("收到来自进程 %lld 的消息:%s\n", msg.msg_type, msg.msg_text);
        }
    }
    return 0;
}

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

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值