糊里糊涂学了大半个学期的计算机操作系统,今天一下午做实验还是没有搞出来,班上的同学基本都做出来给老师检查了,就我磨洋工还没弄出来,老师本以为都能当堂检查,自己觉得很尴尬,于是晚上下定决心要弄出来,终于还是得到了像样的结果,在此记录一下。
实验内容及要求
- 父进程使用系统调用pipe()建立一个管道,然后使用系统调用fork()创建两个子进程:子进程1和子进程2
- 子进程1每隔1秒通过管道向子进程2发送数据:I send you x times.(x初值为1,以后发送一次后做加一操作)子进程2从管道读出信息,并显示在屏幕上
- 父进程用系统调用signal()来捕捉来自键盘的中断信号(即按Ctrl+C键);当捕捉到中断信号后,父进程用系统调用kill()向两个子进程发出信号,子进程捕捉到信号后分别输出如下信息后终止:
Child Process 1 is killed by Parent!
Child Process 2 is killed by Parent! - 父进程等待两个子进程终止后,释放管道并输出如下的信息后终止
Parent Process is Killed!
实验步骤
- 首先根据实验给的pdf最后一张“实验指导”,先写了一个按其提示内容为注释的代码“框架”,如下代码所示
int main()
{
//创建无名管道
//设置软中断信号SIGINT
//创建子进程1,2
//等待子进程1,2退出
//关闭管道
}
- 第一步:创建无名管道,使用管道进行进程间的通信,关于有名管道和无名管道的知识就不讲了,我自己也没看,我就网上百度了下pipe()这个函数,看看别人写的简单范例,大致了解一下pipe(),然后把代码抄了一部分下来,修理一下,我们的main函数代码就变成了这个样子
int main(void)
{
int filedis[2];
int counter;
int child1,child2;
char buffer[40];
//创建无名管道
if(pipe(filedis) < 0)
{
printf("Create pipe failed\n");
return -1;
}
//设置软中断信号SIGINT
//创建子进程1,2
child1 = fork();
if(child1 == 0)
{
printf("Inchild1 process\n");
while(1){
close(filedis[0]);
sprintf(info,"I send you %d times",counter);
write(filedis[1],info,30);
counter++;
sleep(1000);
}
}
child2 = fork();
if(child2 == 0)
{
printf("Inchild2 process\n");
while(1){
close(filedis[1]);
read(filedis[0],buffer,40);
printf("%s\n",buffer);
}
}
//等待子进程1,2退出
//关闭管道
}
好了,以上代码只是个测试,当然,它的运行结果也不一定对,实际上它存在很多漏洞,后面会看到,我们只是熟悉管道怎么用。我们可以看到,使用pipe创建一个管道后,我们就不在需要pipe()了,然后只着重在这个很特殊的二元整型数组filedis[]上。一开始不了解pipe(),就很好奇这个pipe函数怎么偏偏,刚好就是要一个2个元素的整型数组,3个,4个可不可以?答案是不可以(实验资料pdf有讲到pipe,不过自己就是没仔细看,自打脸),这个filedis[0]可以看成一个指针,表示管道的读入端,而filedis[1]则表示管道的写入端,一个读取端口一个写入端口,就像现实中的一根水管一样,你还想来几个口子(3个,4个)岂不是要漏水?所以不多不少就2个。当我们要向管道写点信息的时候,就要用到filedis[1],它作为函数write的一个参数使得我们把信息写到管道里面,这里要注意了!我们说(实际上是资料告诉我说)管道用于父子进程以及兄弟进程之间,它们共享管道,所以要是我们在进程1里面写管道的时候,进程2在读管道怎么办,信息还在发呢,你就迫不及待地读取,能得到完整准确的信息吗?显然不能,所以,在写的时候我们就要把读入端口filedis[0]给关闭了,使用函数close来实现这个功能,代码:`close(filedis[0])`,同样地,在读的时候【read()函数】要关闭写端口。
OK,我们来总结下上面的代码,先用pipe函数创建管道,然后用fork产生两个子进程,子进程1写信息到管道,等1秒sleep(),然后子进程2不断读取管道信息并显示,就干了这么一件事,好了,运行运行看看吧?别呀,这段代码还有几个错误,况且头文件还没加呢!
- 第二步:设置软中断信号
啥叫软信号中断?看看实验资料pdf,这里我们就是要用到一个signal函数,赶紧先百度下signal,自学了点C#,Qt的我也很快理解了下signal,简单来说它有两个参数,第一个是signal接受的信号,一个整型值,在linux中定义个64个这样的整型信号值,就像C语言里面的枚举变量enum,为了好记好写,给他们取个别名,比如这里的SIGINT,就表示Ctrl+C的组合键信号,(pdf里面又有,我又没仔细看,啪啪啪打脸!),第二个参数是一个信号处理函数,也就是信号发生后,signal接收到信号就会让信号处理函数去执行,就是C#,java里面的事件event,或者说更像Qt里面的槽函数slot,其中第二个参数可以写上SIG_IGN
表示忽略这个信号,什么事也不干(看别人范例代码才知道的,不然一开始我还按照pdf讲的填个1,结果编译出错)。再仔细钻研下,我们这个信号处理函数还有点特别,返回类型为void,参数只有一个,就是signal收到的信号,这个特点也一度让我陷入迷茫中。好了,讲了这么多,我们
再向源文件里面加点代码,如下所示
int child1,child2;
void SignHandler1(int iSignNo);
int main(void)
{
int filedis[2];
int counter;
char buffer[40];
//创建无名管道
if(pipe(filedis) < 0)
{
printf("Create pipe failed\n");
return -1;
}
//设置软中断信号SIGINT
signal(SIGINT,SignHandler1);
//创建子进程1,2
child1 = fork();
if(child1 == 0)
{
printf("Inchild1 process\n");
while(1){
close(filedis[0]);
sprintf(info,"I send you %d times",counter);
write(filedis[1],info,30);
counter++;
sleep(1000);
}
}
child2 = fork();
if(child2 == 0)
{
printf("Inchild2 process\n");
while(1){
close(filedis[1]);
read(filedis[0],buffer,40);
printf("%s\n",buffer);
}
}
//等待子进程1,2退出
//关闭管道
}
void SignHandler1(int iSignNo)
{
printf("\nParent receive signal Ctrl+C\n");
if(iSignNo == SIGINT) //传递SIGUSR信号给子进程
{
kill(child1,SIGUSR1);
kill(child2,SIGUSR1);
}
}
迷茫的原因:实验要求主进程接收到SIGINT信号后用kill函数传递信号SIGUSR1到两个子进程中,这里kill函数就需要子进程的PID,也就是child1和child2,可是信号处理函数的参数就只有一个,无法在函数中得到PID,上网查查看到别人说用全局变量,没办法先这么搞吧。。。于是乎上面的代码和第一次添加相比发生了一个重大改动,我们把在main函数体中的child1和child2提到了main函数提外面,变成了全局变量。
- 第三,四步:等待子进程1,2退出,关闭管道
这个很简单,使用wait函数和close函数,这里就不多讲了,添加代码如下:
//等待进程1,2退出
waitpid(child1,NULL,0);
printf("Child Process1 is over\n");
waitpid(child2,NULL,0);
printf("Child Process2 is over\n");
//关闭管道
close(filedis[0]);
close(filedis[1]);
printf("Parent Process is killed!\n");
这就完了吗?还没结束呢!上上步刚刚讲到主进程接收到SIGINT信号后发给子进程1,2一个SIGUSR1信号,现在我们再添加点代码,让子进程处理这个信号,也是按照实验要求,子进程收到这个信号后自行结束。这个一开始我用的kill函数加PID,结果调试运行没有得到正确结果,然后再仔细看看pdf,发现了exit(),于是把kill换成了exit,还真有效!
- 最终章:最后的炸弹
讲到这里,这个实验也差不多该结束了吧,万万没想到,从一开始就给自己埋下了个定时炸弹,这也是我在查fork函数的资料时在别人的博客上看见的,下面的代码片段实际上产生了包含主进程在内一共4个进程!
child1 = fork();
if(child1 == 0)
{
...
}
child2 = fork();
if(child2 == 0)
{
...
}
原因:fork()产生子进程,子进程代码执行从fork()表达式的下一条语句开始,所以子进程1在也会执行child2 = fork();然后就有了第四个进程。如何避免这种现象,要得到只有两个子进程的代码怎么写?这里就要使用if-else语句来解决了,大致代码如下:
child1 = fork();
if(child1 == 0)
{
...
}
else if(child1 > 0) //回到主进程
{
child2 = fork(); //主进程创建第二个子进程
if(child2 == 0)
{
...
}
}
好了,到了这里可以说程序基本完成了,我们运行一下吧,唉?怎么都是1,我要的1,2,3…呢?
错误原因:在linux中sleep的参数是秒,而不是毫秒(在写C#,java里面都是毫秒为单位习惯了),所以sleep(1000)表示停止运行1000秒,这得有将近10分钟,只要把1000改为1就可以了。
Finally,结束了,下面贴上完整代码,这次有头文件了
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
void SignHandler1(int iSignNo);
void SignHandler2(int iSignNo);
int child1,child2;
int filedis[2];
int main(void)
{
char buffer[40];
char info[40];
int status;
int counter = 1;
printf("Process Parent pid %d\n",getpid());
//创建无名管道
if(pipe(filedis) < 0)
{
printf("Create pipe failed\n");
return -1;
}
//设置软中断信号SIGINT
signal(SIGINT,SignHandler1);
//创建进程1,2
child1 = fork();
if(child1 == 0) //子进程1
{
printf("Process1 pid %d\n",getpid());
signal(SIGINT,SIG_IGN);
signal(SIGUSR1,SignHandler2);
while(1){
close(filedis[0]);
sprintf(info,"I send you %d times",counter);
write(filedis[1],info,30);
counter++;
sleep(1);
}
}
else if(child1 >0) //返回到主进程
{
child2 = fork();
if(child2 == 0) //子进程2
{
printf("Process2 pid %d\n",getpid());
signal(SIGINT,SIG_IGN);
signal(SIGUSR1,SignHandler2);
while(1){
close(filedis[1]);
read(filedis[0],buffer,40);
printf("%s\n",buffer);
}
}
//等待进程1,2退出
waitpid(child1,NULL,0);
printf("Child Process1 is over\n");
waitpid(child2,NULL,0);
printf("Child Process2 is over\n");
//关闭管道
close(filedis[0]);
close(filedis[1]);
printf("Parent Process is killed!\n");
}
return 0;
}
void SignHandler1(int iSignNo)
{
printf("\nParent receive signal Ctrl+C\n");
if(iSignNo == SIGINT) //传递SIGUSR信号给子进程
{
kill(child1,SIGUSR1);
kill(child2,SIGUSR1);
}
}
void SignHandler2(int iSignNo)
{
close(filedis[0]);
close(filedis[1]);
if(child1 == 0 && iSignNo == SIGUSR1)
{
printf("Child Process1 is killed by Parent!\n");
exit(0); //子进程1结束
}
if(child2 == 0 && iSignNo == SIGUSR1)
{
printf("Child Process2 is killed by Parent!\n");
exit(0); //子进程2结束
}
}
运行结果如下:
实验小结
实际上在完成实验一达到的运行结果的过程中,还有很多查资料,收获知识的过程,比如sprintf函数就是我在别人的博客上看到了,因为要输入信息包含counter这个整数,而它又是在字符串中间的,我本来是用itoa+strcat函数来产生信息,但是一看到有人说sprintf这个函数,就查了下试用看看,简直惊喜,一条语句就搞得了本来以为很复杂的操作过程,如获至宝!另外看到关于fork连用产生4个进程的博客也解释了为什么我用ps命令看到了4个运行的进程,等等,我感觉到现在为止还是只学了1/4桶水,linux真是太庞大了,一个操作系统的用到各种解决思想也是五花八门,最后留几个问题给日后自己解决(自己以后也不一定搭理):能不能再优化一下代码,比如不要全局变量child1,child2,或者使用更为精炼的方法来实现实验要求?
ps:本文所讲的实验内容pdf已上传至我的资源,感兴趣的同学下载看看,做一做