进程间通讯

一、关于进程间通讯

  • linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的。
  • 每个进程都有自己独立的地址空间,当两个不同进程需要进行交互时,就需要使用进程间通讯
  • 进程间通讯分为单个计算机的进程间通讯与局域网的计算机的进程间通讯
  • 进程间通讯方式有 管道,信号,消息队列,共享内存,网络

二、管道

管道简介

管道分为 无名管道 与 有名管道
无名管道用于父子进程之间通讯
有名管道用于任意进程之间通讯
管道的本质是在内存建立一段缓冲区,由操作系统内核来负责创建与管理,具体通讯模型如下

无名管道

无名管道的特点:

  • 无名管道属于单向通讯
  • 无名管道只能用于 父子进程通讯
  • 无名管道发送端叫做写端,接收端叫做读端
  • 无名管道读端与写端抽象成两个文件进行操作,在无名管道创建成功之后,则会返回读端与写端的文件描述

    创建无名管道需要调用 pipe() 函数
    函数头文件
    #include <unistd.h>
    函数原型
    int pipe(int pipefd[2]);
    函数参数
    pipefd: 用于存储无名管道读端与写端的文件描述符的数组
    pipefd[0]: 读端文件描述符
    pipefd[1]: 写端文件描述符
    函数返回值:
    成功: 0
    失败 : -1,设置 errno
    示例:创建子进程,父进程通过管道向子进程发送"Hello,pipe"
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <wait.h>
#include <stdlib.h>

int main(void){
        pid_t cpid;
        int ret;
        int pipefd[2];

        ret = pipe(pipefd);
        if(ret == -1){
                perror("[ERROR] pipe():");
                exit(EXIT_FAILURE);
        }
        cpid = fork();
        if(cpid == 0){
                ssize_t rbytes;
                char buffer[64] = {0};
                close(pipefd[1]);

                rbytes = read(pipefd[0],buffer,sizeof(buffer));
                if(rbytes == -1){
                        perror("[ERROR] read()");
                        close(pipefd[0]);
                        exit(EXIT_FAILURE);
                }
                printf("buffer: %s\n",buffer);
                close(pipefd[0]);
        }
        else if(cpid > 0){
                ssize_t wbytes;
                char buffer[] = "hello pipe";
                close(pipefd[0]);
                wbytes = write(pipefd[1],buffer,strlen(buffer));
                if(wbytes == -1){
                        perror("[ERROR] write():");
                        wait(NULL);
                        close(pipefd[1]);
                        exit(EXIT_FAILURE);
                }
                close(pipefd[1]);
                wait(NULL);

        }
        return 0;

}

无名管道的特点

  • 当管道为空时,读管道会阻塞读进程
  • 当管道的写端被关闭了,从管道中读取剩余数据后,read 函数返回 0
  • 在写入管道时,确保不超过PIPE BUF字节的操作是原子的
    • 当写入的数据达到PIPE BUF字节时,write()会在必要的时候阻塞直到管道中的可用空间足以原子地完成操作
    • 当写入的数据大于PIPE BUF字节时,write()会尽可能多传输数据以充满这个管道管道的大小是有限的,不能让父 /子进程同时对管道进行读 / 写操作
  • 当一个进程试图想一个管道中写入数据但没有任何进程拥有该管道的打开着的读取描述符,内核向写入进程发送一个SIGPIPE信号

练习

创建一个子进程,负责循环从管道中读取数据,父进程从键盘读取数据后,写入到管道中,输入“quit”字符串时退出

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

int main(void){
        pid_t cpid;
        int ret;
        int pipefd[2];

        ret = pipe(pipefd);
        if(ret == -1){
                perror("[ERROR] pipe():");
                exit(EXIT_FAILURE);
        }
        cpid = fork();
        if(cpid == 0){
                ssize_t rbytes;
                char buffer[64] = {0};
                close(pipefd[1]);
                while(rbytes = read(pipefd[0],buffer,sizeof(buffer))){
                    if(strcmp(buffer,"quit\n") == 0){
                        break;
                    }
                    if(rbytes == -1){
                        perror("[ERROR] read()");
                        close(pipefd[0]);
                        exit(EXIT_FAILURE);
                    }
                    printf("buffer: %s",buffer);
                    memset(buffer,0,64);
                }
                close(pipefd[0]);
        }
        else if(cpid > 0){
                ssize_t wbytes;
                char* buffer = (char*)malloc(100*sizeof(char));
                close(pipefd[0]);
                while(strcmp(buffer,"quit\n") != 0){
                    memset(buffer,0,100);
                    fgets(buffer, 100, stdin);
                    wbytes = write(pipefd[1],buffer,strlen(buffer));
                    if(wbytes == -1){
                        perror("[ERROR] write():");
                        wait(NULL);
                        close(pipefd[1]);
                        exit(EXIT_FAILURE);
                    }
                }
                close(pipefd[1]);
                wait(NULL);
        }
        return 0;

}

有名管道

  • 有名管道是在 文件系统中可见的文件,但是不占用磁盘空间,仍然在内存中,可以通过 mkfifo 命令创建有名管道

  • 有名管道与无名管道一样,在应用层是基于文件接口进行操作

  • 有名管道用于 任意进程之间的通讯,当管道为空时,读进程会阻塞

    创建有名管道需要调用 mkfifo() 函数
    函数头文件
    #include <sys/types.h>
    #include <sys/stat.h>
    函数原型:
    int mkfifo(const char *pathname, mode t mode);
    函数参数:
    pathname: 有名管道路径名
    mode: 有名管道文件访问权限
    函数返回值:
    成功: 返回 0
    失败: 返回 -1,并设置 errno
    示例
    创建两个没有血缘关系的进程,使用有名管道进行进程间通讯

  • read.cpp

#include <iostream>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

#define FIFO_NAME "./fifo"
int main(){
        int fd;
        char rbuffer[64];
        fd = open(FIFO_NAME,O_RDWR);
        if(fd == -1){
                perror("[ERROR] open()");
                exit(EXIT_FAILURE);
        }
        int rbytes;
        rbytes = read(fd,rbuffer,sizeof(rbuffer));
        if(rbytes > 0){
                printf("rbytes : %d rbuffer : %s\n",rbytes,rbuffer);
        }
        close(fd);
        return 0;
}

  • write.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#define FIFO_NAME "./fifo"
int main(){
        int fd;
        char wbuffer[64]="fifo pipe";
        int ret;
        ret = access(FIFO_NAME,F_OK);
        if(ret == -1)
                mkfifo(FIFO_NAME,0644);
        fd = open(FIFO_NAME,O_RDWR);
        if(fd == -1){
                perror("[ERROR] open():");
                exit(EXIT_FAILURE);
        }

        int wbytes;
        wbytes = write(fd,wbuffer,strlen(wbuffer)+1);
        if(wbytes < 0)
                perror("[ERROR] write error");
        close(fd);
        return 0;

}

  • 注意:
    如果有名管道的一端以只读方式打开,它会阻塞到另一端以写的方式(只写,读写)。 如果有名管道的一端以只写方式打开,它会阻塞到另一端以读的方式(只读,读写)

  • 有名管道的优缺点:

    • 优点
      可以实现任意进程间通信,操作起来和文件操作一样
    • 缺点:
      • 打开的时候需要读写一起进行否则就会阻塞,管道大小是 4096个字节
      • 半双工的工作模式,如果和多个进程通信则需要创建多个管道
  • 练习
    设计两个没有血缘关系的进程,使用有名管道一个进程获取当前系统时间给另外一个进程

进程间通讯-信号(一)

关于信号

信号定义

信号是在软件层次上 是一种通知机制,对中断机制的一种模拟,是一种异步通信方式,一般具有如下特点:

  • 进程在运行过程中,随时可能被各种信号打断,
  • 进程可以忽略,或者去调用相应的函数去处理信号
  • 进程无法预测到达的精准时间

信号来源

在 Linux 中信号一般的来源如下:

  • 程序执行错误,如内存越界,数学运算除0
  • 由其他进程发送
  • 通过控制终端发送 如ctrl + c
  • 子进程结束时向父进程发送的SIGCLD信号
  • 程序中设定的定时器产生的SIGALRM信号

信号的种类

在 Linux 系统可以通过 kill -l 命令查看,常用的信号列举如下:

  • SIGINT 该信号在用户键入INTR字符(通常是Ctr-C)时发出,终端驱动程序发送此信号并送到前台进>程中9的每一个进程。
  • SIGQUIT 该信号和SIGINT类似,但由QUIT字符(通常是Ctrl-)来控制
  • SIGILL 该信号在一个进程企图执行一条非法指令时(可执行文件本身出现错误,或者试图执行数据段、堆栈溢出时)发出。
  • SIGFPE 该信号在发生致命的算术运算错误时发出。这里不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术的错误。
  • SIGKILL 该信号用来立即结束程序的运行,并且不能被阻塞、处理和忽略.
  • SIGALRM 该信号当一个定时器到时的时候发出。
  • SIGSTOP 该信号用于暂停一个进程,且不能被阻塞、处理或忽略
  • SIGTSTP 该信号用于交互停止进程,用户可键入SUSP字符时(通常是Ctrl-Z)发出这个信号
  • SIGCHLD 子进程改变状态时,父进程会收到这个信号
  • SIGABRT 进程异常中止
#define SIGHUP 1
#define SIGINT 2
#define SIGQUIT 3
#define SIGILL 4
#define SIGTRAP 5
#define SIGABRT 6
#define SIGIOT 6
#define SIGBUS 7
#define SIGFPE 8
#define SIGKILL 9
#define SIGUSR1 10
#define SIGSEGV 11
#define SIGUSR2 12
#define SIGPIPE 13
#define SIGALRM 14
#define SIGTERM 15
#define SIGSTKFLT 16
#define SIGCHLD 17
#define SIGCONT 18
#define SIGSTOP 19
#define SIGTSTP 20
#define SIGTTIN 21

信号处理流程

信号处理流程包含以下三个

  • 信号的发送: 可以由进程直接发送
  • 信号投递与处理:由内核进行投递给具体的进程并处理
    在 Linux 中对信号的处理方式如下:
  • 忽略信号,即对信号不做任何处理,但是有两个信号不能忽略: 即SIGKILL及SIGSTOP
  • 捕捉信号,定义信号处理函数,当信号发生时,执行相应的处理函数。
  • 执行缺省操作,Linux对每种信号都规定了默认操作

    在内核中的用于管理进程的结构为 task struct,具体定义如下

信号发送

当由进程来发送信号时,则可以调用 kill() 函数与 raise()函数

kill函数

函数头文件
#include <sys/types.h>
#include <signal.h>
函数原型
int kill(pid t pid, int sig);
函数功能
向指定的进程发送一个信号
函数参数
pid : 进程的 id
sig : 信号的 id
函数返回值
成功: 返回 0
失败: 返回 -1,并设置 errno

raise 函数

函数头文件
#include <sys/types.h>
#include <signal.h>
函数原型
int raise(int sig):
函数参数
sig : 信号编号
函数返回值
成功 :返回 0
失败: 返回 -1,并设置 errno

  • 创建一个子进程,子进程通过信号暂停,父进程发送 终止信号
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>

int main(){
        pid_t cpid;
        cpid = fork();
        if(cpid  < 0){
                perror("[ERROR] fork():\n");
                exit(0);
        }
        else if(cpid == 0){
                fprintf(stdout,"\t child %d running.\n",getpid());
                raise(SIGSTOP);
                fprintf(stdout,"\t child %d exit \n",getpid());
                exit(EXIT_SUCCESS);
        }
        else if(cpid > 0){
                int status;
                int ret;
                sleep(1);
                ret = kill(cpid,SIGKILL);
                if(ret == 0){
                        fprintf(stdout,"Father %d killed child %d \n",getpid(),cpid);
                }
                waitpid(cpid,NULL,0);
                fprintf(stdout,"father %d exit\n",getpid());
                exit(EXIT_SUCCESS);

        }
        return 0;

}

等待信号

在进程没有结束时,进程在任何时间点都可以接受到信号
需要阻塞等待信号时,则可以调用 pause()函数,具体如下
函数头文件
#include <unistd.h>
函数原型
int pause(void);
函数功能
阻塞进程,直到收到信号后唤醒
函数返回值
成功 :返回 0
失败 : 返回 -1,并设置 errno

  • 创建创建一个子进程,父进程调用 pause 函数,子进程给父进程发送信号
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>

int main(){
        pid_t cpid;
        cpid = fork();
        if(cpid  < 0){
                perror("[ERROR] fork():\n");
                exit(0);
        }
        else if(cpid == 0){
                fprintf(stdout,"\t child %d running.\n",getpid());
                sleep(3);
                kill(getppid(),SIGUSR1);
                exit(EXIT_SUCCESS);
        }
        else if(cpid > 0){
                sleep(1);
                fprintf(stdout,"Main Process Start\n");
                pause();
                fprintf(stdout,"Main Process End\n");
                exit(EXIT_SUCCESS);

        }
        return 0;
  }
  • 创建两个子进程,由父进程分别给两个子进程发送 SIGKILL 信号
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>

int main(){
        pid_t cpid,cpid2;
        cpid = fork();
        if(cpid >0)
                cpid2 = fork();
        if(cpid  < 0 || cpid2 < 0){
                perror("[ERROR] fork():\n");
                exit(0);
        }
        else if(cpid == 0){
                fprintf(stdout,"\t child %d running.\n",getpid());
                pause();
                exit(EXIT_SUCCESS);
        }
        else if(cpid2 == 0){
                fprintf(stdout,"\t child %d running.\n",getpid());
                pause();
                exit(EXIT_SUCCESS);
        }
        else if(cpid > 0 && cpid2 > 0){
                int ret1,ret2;
                fprintf(stdout,"Main Process Start\n");
                sleep(3);
                ret1 = kill(cpid,SIGKILL);
                ret2 = kill(cpid2,SIGKILL);
                fprintf(stdout,"<%d> <%d> have been killed,ret <%d> <%d>",cpid,cpid2,ret1,ret2);
                fprintf(stdout,"Main Process End\n");
                exit(EXIT_SUCCESS);

        }
        return 0;

}

用户自定义处理

  • 用户自定义处理基本的流程
    • step 1:实现自定义处理函数
      用户实现自定义处理函数,需要按照下面的形式定义
      typedef void (sighandler t)(int);
      typedef void (
      )(int) sighandler_t
    • step 2: 设置信号处理处理方式
      通过 signal 函数设置信号外理方式
      函数头文件
      #include <signal.h>
      函数原型
      sighandler t signal(int signum, sighandler_t handler);
      函数功能
      设置信号的处理方式,如果是自定义处理方式,提供函数地址,注册到内核中
      函数参数
      • signum : 信号编号
      • handler : 信号处理方式
        • SIG_IGN: 忽略信号
        • SIG_DFL: 按照默认方式处理
        • 自定义处理函数的地址
typedef void _signalfn_t(int);
typedef _signalfn_t *_sighandler_t;
typedef void _restorefn_t(void);
typedef _restorefn_t *_sigrestore_t;
#define SIG_DFL (( _sighandler t)o) /* default signal handling */
#define SIG_IGN (( _sighandler t)1) /* ignore signal */
#define SIG_ERR (( _sighandler t)-1) /* error return from signal */

函数返回值

  • 成功:返回信号处理函数地址
  • 失败:返回 SIG_ERR ,并设置 errno
    要点
    三种信号处理方式处理,一般选择一种即可
    示例
    创建一个子进程,父进程给子进程发送 SIGUSR1 信号,并使用自定义的处理方式
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <string.h>
void do_sig_usr(int sig){
        printf("Receive %s \n",strsignal(sig));
}
int main(){
        pid_t cpid;
        if(signal(SIGUSR1,do_sig_usr) == SIG_ERR){
                perror("[ERROR] signal():");
                exit(EXIT_FAILURE);
        }
        cpid = fork();
        if(cpid  < 0){
                perror("[ERROR] fork():\n");
                exit(0);
        }
        else if(cpid == 0){
                fprintf(stdout,"\t child %d running.\n",getpid());
                pause();
                fprintf(stdout,"pause end \n");
                exit(EXIT_SUCCESS);
        }
        else if(cpid > 0){
                sleep(1);
                fprintf(stdout,"Main Process Start\n");
                kill(cpid,SIGUSR1);
                wait(NULL);
                fprintf(stdout,"Main Process End\n");
                exit(EXIT_SUCCESS);

        }
        return 0;

}
  • 练习
    创建两个子进程 A与 B,给子进程 A 发送 SIGUSR1 信号,子进程 B 发送 SIGUSR2 信号,子进程A的处理方式设置为默认,子进程B的处理方式为使用自定义处理函数,并打印接收的信号的字符串信息(使用strsignal()函数)
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <string.h>
void do_sig_usr(int sig){
        printf("Receive %s \n",strsignal(sig));
}
int main(){
        pid_t cpid,cpid2;
        cpid = fork();
        if(cpid > 0){
                cpid2 = fork();
                if(signal(SIGUSR1,do_sig_usr) == SIG_ERR){
                        perror("[ERROR] signal():");
                        exit(EXIT_FAILURE);
                }
        }
        if(cpid  < 0){
                perror("[ERROR] fork():\n");
                exit(0);
        }
        else if(cpid == 0){
                fprintf(stdout,"\t child %d running.\n",getpid());
                pause();
                fprintf(stdout,"pause end \n");
                exit(EXIT_SUCCESS);
        }
        else if(cpid2 == 0){
                fprintf(stdout,"\t child %d running.\n",getpid());
                pause();
                fprintf(stdout,"pause end \n");
                exit(EXIT_SUCCESS);
        }
        else if(cpid > 0){
                sleep(1);
                fprintf(stdout,"Main Process Start\n");
                kill(cpid,SIGUSR1);
                kill(cpid2,SIGUSR1);
                wait(NULL);
                wait(NULL);
                fprintf(stdout,"Main Process End\n");
                exit(EXIT_SUCCESS);

        }
        return 0;

}

定时器信号

在 Linux 系统中提供了 alarm 函数,用于设置定时器,具体信息如下
函数头文件
#include <unistd.h>
函数原型
unsigned int alarm(unsigned int seconds);
函数功能
设置定时器的秒数
函数参数
seconds: 定时的时间秒数
函数返回值
返回上一次进程设置定时器剩余的秒数
要点:
定时器的定时任务由内核完成,alarm 函数值负责设置定时时间,并告诉内核启动定时器
当定时时间超时后,内核会向进程发出 SIGALRM 信号

子进程退出信号

问题:

在使用 wait()函数时,由于阻塞或者非阻塞都非常消耗资源,并且在阻塞情况下,父进程不能执行其他逻辑

解决方案

子进程退出是异步事件,可以利用在子进程退出时,会自动给父进程发送 SIGCHLD 信号

使用信号处理僵死态进程

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>

void do_sig_child(int sig){
        fprintf(stdout, "Receive signal <%s> \n",strsignal(sig));
        wait(NULL);
        exit(0);
}
int main(void){
        pid_t cpid;
        cpid = fork();
        if(signal(SIGCHLD,do_sig_child) == SIG_ERR){
                perror("[ERROR] signal():");
                exit(EXIT_FAILURE);
        }
        if(cpid == -1){
                perror("[ERROR] fork()");
                exit(EXIT_FAILURE);
        }else if(cpid == 0){
                printf("Child process <%d> start\n",getpid());
                sleep(2);
                exit(EXIT_SUCCESS);
        }else{
                while(1){
                }

        }
        return 0;

}

练习

探测用户是否已经输入,如果用户在3秒内没有输入则提示超时一次,如果超时三次程序自动结束.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>

static int i = 0;

void do_sig_child(int sig){
        fprintf(stdout, "Receive signal <%s> \n",strsignal(sig));
        if(i < 3){
                fprintf(stdout,"3 seconds not input\n");
                alarm(3);
                i ++;
        }
        else{
                fprintf(stdout,"time limited\n");
                exit(0);
        }
}
int main(void){
        pid_t cpid;
        char streams[1024];
        if(signal(SIGALRM,do_sig_child) == SIG_ERR){
                perror("[ERROR] signal():");
                exit(EXIT_FAILURE);
        }
        alarm(3);
        fgets(streams,1024,stdin);
        return 0;

}
posted @ 2023-04-01 21:32  shubin  阅读(32)  评论(0编辑  收藏  举报