第三组 进程通信之管道通信与信号通信
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秒后子进程释放锁,得到的结果是:
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的管道文件。
使用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
,使得写者程序再前台运行。从键盘输入内容,读者程序会将内容打印。键入回车终止。
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
得到的结果是:
多次执行结果,可以看到两个子进程接收信号的顺序是不固定的。
4.4 实验总结
在实验过程中,我发现,在键盘给出中断信号时,主进程和两个子进程都会收到中断信号。如果不使用signal()捕获信号,则进程会被关闭。如果使用signal()捕获信号,则进程的pause()会被打断。为了使子进程等待信号16/17再退出,我的想法是,可以在子进程中连续使用两个pause(),第一个pause()被键盘的中断信号打断后,再pause()一次,等待来自父进程的kill信号。
但是这样设计会出现以下问题:在键盘按下中断后,子进程一直没有抢到时间片,从而子进程的第一个pause()还没有被打断时,主进程就已经用kill()发送了信号。当子进程抢到了时间片时,来自键盘的中断信号和来自父进程的信号一同到达,因此只打断了一个pause(),没有打断第二个pause()。
为了解决这个问题,我发现可以在子进程中设置signal(SIGINT, SIG_IGN),忽略SIGINT信号,这样子进程只需要使用一次pause(),等待来自父进程的kill()信号即可。
本实验花费了我约一个小时半完成,这是因为我已经在第一个实验中掌握了kill()与signal()系统调用的原理和使用方法,因此在看到本题题目时我就已经有了大致的结题思路。本实验加深了我对信号通信的理解,理解了如何使用pause(),为了忽略信号SIGINT,我还了解到了关于sigsuspend()的使用。但是在研究的过程中,我发现实验指导书中提供的系统调用已经足够完成任务,使用SIG_IGN忽略信号即可。本次实验室一次非常新颖的体验,让我对操作系统的了解更近了一步,让我受益匪浅。
Comments NOTHING