【北邮大三下操作系统】第三组 进程通信——管道通信与信号通信

发布于 2023-07-21  615 次阅读


第三组 进程通信之管道通信与信号通信

1 实验目的与要求

查阅资料,学习掌握 Linux 系统提供的用于四种进程间通信的系统调用、库函数的使用方法和参数,参照样例程序,设计完成以下四组实验。

2 实验环境

硬件:阿里云轻量型服务器,2核CPU,内存2G

软件:CentOS 8.2,内核版本为4.18,gcc编译器

3 实验内容三:管道通信

要求:编程实现进程间的(无名)管道、命名管道通信,方式如下。

(1)管道通信:

父进程使用 fork()系统调用创建子进程;

子进程作为写入端,首先将管道利用 lockf()加锁,然后向管道中写入数据,并解锁管道;

父进程作为读出端,从管道中读出子进程写入的数据。

(2)命名管道通信:

读者进程使用 mknod 或 mkfifo 创建命名管道;

读者进程、写者进程使用 open()打开创建的管道文件;

读者进程、写者进程分别使用 read()、write()读写命名管道文件中的内容;

通信完毕,读者进程、写者进程使用 close 关闭命名管道文件。

3.1 无名管道通信

3.1.1 实验思路

创建管道

根据实验要求,创建一根单工通信的管道。

int fd[2];
pipe(fd);

父进程通过fork创建子进程

父进程通过调用fork()创建子进程,如果创建失败,则再次创建,直到成功为止。

子进程写入

子进程首先调用lockf(fd[1], F_LOCK, 0)给管道加锁,然后使用write()向管道中写数据,sleep()3秒后使用lockf(fd[1], F_ULOCK, 0)给管道解锁。注:经过测试,不能给fd[0]加锁

父进程读出

经过测试,父进程使用read()读取管道中的数据不受lockf()上锁的影响。如果管道中没有数据,read()会被阻塞,一旦管道中有数据,则数据会被读出。如果要使父进程等子进程解锁后再读数据,需要调用lockf(fd[0], F_TEST, 0)判断管道是否被上锁,如果返回-1,说明管道被上锁。

3.1.2 具体实现
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
    int pid;
    int fd[2];
    char outpipe[100], inpipe[100];
    if (pipe(fd) < 0) {
        perror("pipe error");
    }  /*创建一个管道*/
    while ((pid = fork())==-1); /*如果进程创建不成功,则空循环*/
    if (pid == 0) {
        if (lockf(fd[1], F_LOCK, 0) < 0) {
            perror("lockf1 failed");
        }  /*锁定管道*/
        if (read(0, outpipe, 100) < 0) {
            perror("read from stdin is failed");
            exit(1);
        }  /*从stdin获取内容*/
        write(fd[1], outpipe, 100);  /*向管道写长为100字节的串*/
        sleep(3);
        lockf(fd[1], F_ULOCK, 0);  /*解除管道的锁定*/
        exit(0);
    } else {
        sleep(1);
        while (lockf(fd[0], F_TEST, 0) == -1) {
            sleep(0.5);
        }  /*等待管道解除锁定*/
        if (read(fd[0], inpipe, 100) <= 0) {
            printf("read failed");
        }  /*从管道中读长为100字节的串*/
        printf("Parent read message: %s\n", inpipe);  /*显示读出的数据*/
        wait(0);  /*回收子进程*/
        exit(0);
    }
}
3.1.3 运行结果

先使用gcc编译器通过以下指令编译程序:

gcc pipe.c -o pipe

然后执行./pipe,输如helloworld !,等待3秒后子进程释放锁,得到的结果是:

image-20221209210943103

3.1.4 实验总结

本实验我共花费了大约两个小时完成,在此有个建议:

实验指导书中的样例程序并不能体现出加锁的用途:父进程是通过wait()等待子进程结束之后再读,从而避免的读写冲突,这与子进程是否上锁就毫无关系了。我尝试将read()放至wait()之前,父进程可以在子进程解锁之前就读取到数据。经过查阅网上资料,得到如下解释:“如果管道中没有数据,read()会被阻塞,一旦管道中有数据,则数据会被读出”。所以似乎不需要加锁,管道本身就有自己的同步机制。为什么需要锁,锁的目的是什么,我想要深入探究,但是一头雾水。另外实验指导书中关于lockf()的讲解也远不如signal()详细,希望指导老师可以将此部分写得更详细一些。

3.2 命名管道通信

3.2.1 实验思路

mkfifo创建命名管道文件

单独一个程序,调用mkfifo()创建一个fifo管道文件。创建完成后可以反复使用,不需要重复创建。

读者写者进程打开创建的管道文件

命名管道可以让非父子关系的进程进行通信。因此我们写两个程序,一个程序作为写者,另一个程序作为读者。写者程序和读者程序都调用open()打开管道。由于管道通信具有及时性,读端和写端必须同时打开,如果有一方没有调用open(),则打开了管道的一方被阻塞。因此我们需要同时运行这两个程序。在Linux中断下,可以将读者程序调入后台运行。

读者写者进程通信

读者进程和写者进程分别使用 read()、write()读写命名管道文件中的内容。我们让写者程序反复从键盘中读取内容,如果用户不输入内容直接敲回车,则退出循环。读者程序也反复从管道中读取信息,当读取到文件末尾,则退出循环。

读者写者进程关闭管道

读者与写者分别调用close()关闭命名管道文件。

3.2.2 具体实现

fifo.c

#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include <stdlib.h>

int main() {
    umask(0);
    if (mkfifo("./fifo_pipe", S_IFIFO | 0666) == -1) {
        perror("mkfifo error!");
        exit(1);
    }
    printf("create fifo seccess\n");
}

fifo_writer.c

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define PATH "./fifo_pipe"
#define SIZE 128
int main() {
    int fd = open(PATH, O_WRONLY);
    printf("Writer is ready to write\n");
    if (fd < 0) {
        perror("open error");
        exit(1);
    } 
    char Buf[SIZE];
    sleep(0.5);
    while (1) {
        printf("Writer: please Enter#:");
        fflush(stdout);
        ssize_t s = read(0, Buf, sizeof(Buf));  // from stdin
        if (s < 0) {
            perror("read from stdin is failed");
            if (close(fd) < 0) {
                perror("close file failed");
            }
            exit(1);
        } else if (s == 1 || s == 0) {
            printf("Writer: I am going to quit!\n");
            break;
        } else {
            Buf[s] = '\0';
            if (write(fd, Buf, strlen(Buf)) < 0) {
                perror("write to fifo is failed");
                if (close(fd) < 0) {
                    perror("close file failed\n");
                }
                exit(1);
            }
            sleep(1);
        }
    }
    if (close(fd) < 0) {
        perror("writer close file failed");
        exit(1);
    }
    return 0;
}

fifo_writer.c

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define PATH "./fifo_pipe"
#define SIZE 128
int main() {
  int fd = open(PATH, O_WRONLY);
  printf("Writer is ready to write\n");
  if (fd < 0) {
    perror("open error");
    exit(1);
  } 
  char Buf[SIZE];
  sleep(0.5);
  while (1) {
    printf("Writer: please Enter#:");
    fflush(stdout);
    ssize_t s = read(0, Buf, sizeof(Buf));  // from stdin
    if (s < 0) {
      perror("read from stdin is failed");
      if (close(fd) < 0) {
        perror("close file failed");
      }
      exit(1);
    } else if (s == 1 || s == 0) {
      printf("Writer: I am going to quit!\n");
      break;
    } else {
      Buf[s] = '\0';
      if (write(fd, Buf, strlen(Buf)) < 0) {
        perror("write to fifo is failed");
        if (close(fd) < 0) {
          perror("close file failed\n");
        }
        exit(1);
      }
      sleep(0.8);
    }
  }
  if (close(fd) < 0) {
    perror("writer close file failed");
    exit(1);
  }
  return 0;
}
3.2.3 运行结果

先使用gcc编译器通过以下指令编译程序:

gcc fifo.c -o fifo

然后执行./fifo,再使用ls查看文件夹下的文件,可以看到生成了名为fifo_pipe的管道文件。

image-20221209220629364

使用gcc编译器通过以下指令编译程序:

gcc fifo_reader.c -o fifo_reader
gcc fifo_writer.c -o fifo_writer

执行./fifo_reader,按ctrl+Z将读者程序暂停并挂到后台,再执行bg 1,使读者程序再后台运行(1是job编号,可用命令jobs查看)。

再执行./fifo_writer,使得写者程序再前台运行。从键盘输入内容,读者程序会将内容打印。键入回车终止。

image-20221209221525461

3.2.4 实验总结

本实验花费了我大约一个半小时完成。在完成本实验的过程中,我加深了对管道通信的理解,让我受益匪浅。

4 实验内容四:信号 signal 通信

使用信号相关的系统调用,参照后续样例程序展示的系统调用使用方法,设计实现基于信号 signal 的进程通信。例如:

使用系统调用 fork()函数创建两个子进程,再用系统调用 signal()函数让父进程捕捉键盘上来的中断信号(即按 Del 键),当父进程接收到这两个软中断的其中某一个后,父进程用系统调用 kill()向两个子进程分别发送整数值为 16 和 17 软中断信号,子进程获得对应软中断信号后,分别输出下列信息后终止。

Child process 1 is killed by parent!!

Child process 2 is killed by parent!!

父进程调用 wait() 函数等待两个子进程终止后,输出以下信息后终止。

Parent process is killed!!

多运行几次编写的程序,简略分析出现不同结果的原因。

4.1 实验思路

父进程通过fork创建子进程

父进程调用fork,创建子进程1后,再调用一次fork,创建子进程2。子进程被创建后,立刻使用getpid()输出自己的进程号,并使用getppid()输出父进程的进程号。父进程创建完两个子进程后,也使用getpid()输出父进程的进程号。

父进程捕获中断信号

调用通过signal(SIGINT, func),捕获中断信号。之后父进程调用pause(),使得父进程进入睡眠,直到收到一个信号为止。通过键盘键入ctrl+C,父进程将收到一个SIGINT信号,信号被捕捉,并且使得 pause 退出等待状态。

子进程捕获父进程发送的信号

两个子进程调用signal(SIGINT, SIG_IGN),屏蔽来自键盘的中断信号。两个子进程分别调用signal(16, func)和signal(17, func),捕获信号16和17。随后两个子进程分别调用pause(),进入睡眠状态。当父进程分别向两个子进程发送kill(pid1, 16)和kill(pid2, 17),两个子进程将分别收到信号16或信号17,信号被捕获,并且使得pause退出等待状态。

父进程等待子进程退出

父进程使用wait()获取子进程退出状态,用WIFEXITED()和WIFSIGNALED()分别判断子进程是正常退出/被信号终止退出,用WEXITSTATUS()和WSTOPSIG()分别获取子进程退出的退出状态码/终止信号。

4.2 具体实现

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

void func(int);

int main(int argc, char *argv[]) {
  pid_t pid1;
  pid_t pid2;

  printf("Parent process main begin and fork before... \n");
  pid1 = fork();
  if (pid1 == -1) {
    perror("Failed to create child1");
    exit(1);
  }
  if (pid1 == 0) {
    // Child1
    printf("Child1 process's PID is %d. My parent's PID is %d.\n", getpid(),
           getppid());
    printf("Child1 begin to pause ...\n");
    signal(SIGINT, SIG_IGN);
    signal(16, func);
    pause();
    printf("Child process 1 is killed by parent!!\n");
    exit(0);
  } else {
    // Parent
    printf("fork after and in else Parent process's PID is %d.\n", getpid());
    pid2 = fork();
    if (pid2 == -1) {
      perror("Failed to create child2");
      exit(1);
    }
    if (pid2 == 0) {
      // Child2
      printf("Child2 process's PID is %d. My parent's PID is %d.\n", getpid(),
             getppid());
      printf("Child2 begin to pause ...\n");
      signal(SIGINT, SIG_IGN);
      signal(17, func);
      pause();
      printf("Child process 2 is killed by parent!!\n");
      exit(0);
    } else {
      // Parent
      printf("Parent begin to pause ... press del to continue.\n");
      signal(SIGINT, func);
      pause();
      printf("Parent awake\n");
      sleep(0.5);
      printf("Parent: Signal %d will be send to child1!\n", 16);
      if (kill(pid1, 16) == 0) {
        //printf("send seccess\n");
      } else {
        printf("send failed1\n");
      }
      printf("Parent: Signal %d will be send to child2!\n", 17);
      if (kill(pid2, 17) == 0) {
        //printf("send seccess\n");
      } else {
        printf("send failed2\n");
      }
      int status;
      //printf("Parent process is waiting for child1 ... \n");
      waitpid(pid1, &status, 0);
      if (WIFEXITED(status)) {
        printf("In parent process, child1 exited with exit code:%d\n",
               WEXITSTATUS(status));
      } else if (WIFSIGNALED(status)) {
        printf("In parent process, child1 killed with signal:%d\n",
               WTERMSIG(status));
      } else if (WIFSTOPPED(status)) {
        printf("In parent process, child1 pause with signal:%d\n",
               WSTOPSIG(status));
      } else {
        printf("In parent process, child1 exited with unknown error\n");
      }
      //printf("Parent process is waiting for child2 ... \n");
      waitpid(pid2, &status, 0);
      if (WIFEXITED(status)) {
        printf("In parent process, child2 exited with exit code:%d\n",
               WEXITSTATUS(status));
      } else if (WIFSIGNALED(status)) {
        printf("In parent process, child2 killed with signal:%d\n",
               WTERMSIG(status));
      } else if (WIFSTOPPED(status)) {
        printf("In parent process, child2 pause with signal:%d\n",
               WSTOPSIG(status));
      } else {
        printf("In parent process, child2 exited with unknown error\n");
      }
      printf("Parent process is killed!!\n");
    }
  }
}

void func(int sig) {
  switch (sig) {
    case SIGINT:
      printf("Catch a signal SIGINT in pid = %d\n", getpid());
      break;
    case 16:
      printf("Catch a signal 16 in pid = %d\n", getpid());
      break;
    case 17:
      printf("Catch a signal 17 in pid = %d\n", getpid());
      break;
    default:
      break;
  }
}

4.3 运行结果

先使用gcc编译器通过以下指令编译程序:

gcc signal.c -o signal

然后执行./signal得到的结果是:

image-20221209122136320

多次执行结果,可以看到两个子进程接收信号的顺序是不固定的。

image-20221209144927692

4.4 实验总结

在实验过程中,我发现,在键盘给出中断信号时,主进程和两个子进程都会收到中断信号。如果不使用signal()捕获信号,则进程会被关闭。如果使用signal()捕获信号,则进程的pause()会被打断。为了使子进程等待信号16/17再退出,我的想法是,可以在子进程中连续使用两个pause(),第一个pause()被键盘的中断信号打断后,再pause()一次,等待来自父进程的kill信号。

image-20221209120307507

但是这样设计会出现以下问题:在键盘按下中断后,子进程一直没有抢到时间片,从而子进程的第一个pause()还没有被打断时,主进程就已经用kill()发送了信号。当子进程抢到了时间片时,来自键盘的中断信号和来自父进程的信号一同到达,因此只打断了一个pause(),没有打断第二个pause()。

image-20221209120219706

为了解决这个问题,我发现可以在子进程中设置signal(SIGINT, SIG_IGN),忽略SIGINT信号,这样子进程只需要使用一次pause(),等待来自父进程的kill()信号即可。

本实验花费了我约一个小时半完成,这是因为我已经在第一个实验中掌握了kill()与signal()系统调用的原理和使用方法,因此在看到本题题目时我就已经有了大致的结题思路。本实验加深了我对信号通信的理解,理解了如何使用pause(),为了忽略信号SIGINT,我还了解到了关于sigsuspend()的使用。但是在研究的过程中,我发现实验指导书中提供的系统调用已经足够完成任务,使用SIG_IGN忽略信号即可。本次实验室一次非常新颖的体验,让我对操作系统的了解更近了一步,让我受益匪浅。