学生党一枚,很多概念都处于一知半解的状态,理解不是很清晰,代码存在一些bug,文章中也进行了详细叙述。这是第一次发博文,排版可能不是很好看,抱以记录及以学习态度,如果文章有任何不对的地方,欢迎指正,十分感谢!
二、项目介绍在linux系统下实现两位用户端利用flock文件锁相互对对方状态(死亡、存活)进行监听,当一方死亡后另外一方用fork及execlp函数实现复苏。当两位用户都处于存活状态下时利用消息队列进行通信。文章涉及知识点包括:flock文件锁使用、fork与execlp函数的联合使用、消息队列通信(不作叙述,需要自行了解)。
文件目录:
我们需要解决的关键性问题如下:如何启动一个进程、如何知道一个进程的状态(死亡、存活)。
三、基础知识介绍以及程序框架梳理 1.fork函数fork函数用于产生一进程(父进程)的副本进程(子进程),执行fork函数后,子进程会包含父进程的所有内容,包括数据空间、执行位置等。子进程和父进程的区分通过fork()返回值来确定,在子进程中,fork函数返回0,父进程中返回子进程的PID。因为复制了所有内容,所以我们只需要加上一个简单if(pid== )的判断就可以分别在父进程与子进程中写入代码。fork函数基本结构如下:
#include2.execlp函数#include int main() { pid_t pid; pid=fork();//执行后即产生子进程,其与父进程除PID外完全相同 if(pid==0)//在子进程中返回0 { printf("this is son processn"); } else //在父进程中返回子进程的进程号(PID) { printf("this is dad processn"); } return 0; }
execlp函数是exec函数族的一名成员 。
定义如下:
int execlp(const char *file,const char *arg,......);
头文件:#include
功能:从PATH环境变量中查找符合参数file的文件名并执行,arg为启动程序所带有的参数,可以传递到目标程序,一般第一个为执行命令名,最后一个为NULL。
所以不难看出,通过fork函数与execlp函数的联合使用,我们能够实现client1启动client2,对于client2来说同理,如此我们解决了启动问题。
修改fork代码如下:
if(pid==0)//在子进程中返回0
{
printf("this is son processn");
execlp("/home/linux/HW/experiments/3multi_process/client2"," start-client2 ",NULL);
}
执行程序后,我们可以通过ps -ef命令看见一个名为 start-client2的命令 产生了一个子进程
3.flock函数如何在代码中判断一个进程的活动状态办法比较多,由于笔者也是第一次学习,尝试过pipe管道搭建、waitpid()函数获取子进程信息、system调用等方式。但是由于自身功底比较薄弱,对于阻塞、非阻塞的问题处理不好导致上述方式均以失败告终。如果成功以其他方式实现的,欢迎留言,我也多学习一下。最后我还是选择了利用flock建议锁来实现对进程状态控制,可以实现双方的状态判断。
关于互斥锁与建议锁的区别我就不作详细叙述了,百度上很多。我采用的是通过flock函数建立的建议锁。flock函数详情如下:
函数详情定义:
int flock(int fd,int operation);
头文件:#include
功能:依照operation所指定的方式对fd所指定的文件进行锁定或解除锁定。
operation参数:
LOCK_SH:共享锁定,多个进程可对同一个文件进行锁定。
LOCK_EX:互斥锁定,一个文件只能又一个互斥锁定。
LOCK_UN:解锁。
LOCK_NB:当无法锁上时,返回进程继续运行,非阻塞方式。
文件上锁:阻塞方式: flock(fd, LOCK_EX)
成功返回0,并且上锁,未成功则一直等待。
非阻塞方式:flock(fd,LOCK_EX|LOCK_NB)
我们可以利用 flock(fd,LOCK_EX|LOCK_NB)监听一个锁的状态,当锁已存在时,函数返回值为-1;当锁未存在时,返回值为0,并且将锁进行上锁。
解锁:1 flock(fp, LOCK_UN);
2 close(fd);意味着进程终止之后锁会解开。(这个地方我们会遇到关于fork的问题,下文中会介绍,读者可以先自己想想)
小结:那么如何判断进程的状态呢?根据flock文件锁性质,我们不难想出,只要我们在每个进程开始时将文件锁上,因为进程终止时文件锁会自动释放,所以我们可以在另一个进程中监听文件锁的
状态,如果发现锁是打开状态,证明进程存活,锁关闭,则进程终止,利用前面所述fork、execlp函数重启进程即可。
四、程序逻辑与实现 1.逻辑梳理经过前面的了解我们简单梳理一下现在的程序逻辑:
对client1:(client2类似)
1.建立文件锁locka(对于client2来说即建立lockb)
2.检测 lockb是否锁上,若锁上跳转3,若未锁上,跳转4
3.即进程client2正在运行,发送信息即可,结束后循环到2
4.即进程2不存在或者死亡,利用fork函数创建子进程client2,结束后父进程循环到2,子进程运行client2。
对server:(非重点,简单介绍即可,后文也会直接给代码)
1.接收client1信息,发给client2
2.接收client2信息,发给client1
3.循环到1
2.程序框架搭建与测试
因为server非重点,所以我们测试的时候先测试进程间的互启动,不考虑消息传递。
注意!!!:一定要注意fork循环互启动很可能会导致fork炸弹的产生,笔者在最初测试时电脑也曾直接死机,所以务必理清逻辑后进行测试(当然对于大佬来说可不必,笔者有点菜,处于初学阶段)。避免办法可参考百度(例如设置用户最大进程数、加入截断增长设置,防止指数级增长以提供反应时间)。
测试环境如下:
(1)创建locka、lockb文件。
(2)编写client1代码如下:
#include#include #include #include int main() { pid_t pid; int lockfda = open("locka", O_WRONLY); int lockfdb = open("lockb", O_WRONLY); //int flags = fcntl(lockfda,F_GETFD); //flags |= FD_CLOEXEC; //fcntl(lockfda,F_SETFD,flags); if(flock(lockfda,LOCK_EX|LOCK_NB)==-1)//锁上文件锁locka { printf("time error:can't lock lockan"); exit(0); } while( 1 ) { if(flock(lockfdb,LOCK_EX|LOCK_NB)==-1) {printf("client2 is runningn");sleep(1);}//查询文件锁lockb状态,返回-1时证明进程正在运行 else { flock(lockfdb, LOCK_UN);//注意因为flock(lockfdb,LOCK_EX|LOCK_NB)==0时也会将lockb锁上,而我们需要client2锁lockb,故这里需要对lockb进行解锁 printf("process client2 is stopped and prepare to restart client2n"); sleep(3); pid=fork(); if(pid==0) { printf("start client2 successfullyn"); execlp("/home/linux/CSDN_test/multi_process/client2"," start-client2 ",NULL); } else { sleep(1); //这个sleep(1)是必要的,关于子进程父进程执行顺序笔者不是很清楚,加上sleep(1)后,才会给client2锁上lockb时间,否则容易导致再次循环判断 flock(lockfdb,LOCK_EX|LOCK_NB)仍然为0导致fork炸弹 } } } return 0; }
(3)编写client2代码如下:
#include#include #include #include int main() { pid_t pid; int lockfda = open("locka", O_WRONLY); int lockfdb = open("lockb", O_WRONLY); //int flags = fcntl(lockfda,F_GETFD); //flags |= FD_CLOEXEC; //fcntl(lockfda,F_SETFD,flags); if(flock(lockfdb,LOCK_EX|LOCK_NB)==-1) { printf("time error:can't lock lockbn"); exit(0); } while( 1 ) { if(flock(lockfda,LOCK_EX|LOCK_NB)==-1) {printf("client1 is runningn");sleep(1);} else { flock(lockfda, LOCK_UN); printf("process client1 is stopped and prepare to restart client1n"); sleep(3); pid=fork(); if(pid==0) { printf("start client1 successfullyn"); execlp("/home/linux/CSDN_test/multi_process/client1"," start-client1 ",NULL); } else { sleep(1); } } } }
测试1:gcc编译直接运行进程client1
linux@ubuntu64-vm:~/CSDN_test/multi_process$ gcc -o client1 client1.c linux@ubuntu64-vm:~/CSDN_test/multi_process$ gcc -o client2 client2.c linux@ubuntu64-vm:~/CSDN_test/multi_process$ ./client1 process client2 is stopped and prepare to restart client2 start client2 successfully client2 is running client1 is running client2 is running client1 is running
测试2: kill client2
打开另一个终端输入:ps -ef查看进程
linux 3291 2978 0 20:51 pts/1 00:00:00 ./client1 linux 3292 3291 0 20:52 pts/1 00:00:00 start-client2
从中我们可以看到./client1产生子进程号为3291的进程,(它同样是由fork命令产生,其父进程号为2978)。进程client1(pid=3291)调用fork,通过execlp传递参数start-client2产生子进程号为3292的进程client2。使用kill -9 3292杀死client2结果如下:
client2 is running client1 is running process client2 is stopped and prepare to restart client2 start client2 successfully client1 is running client2 is running
杀死client2后,client1再次启动client2,查看进程信息可以看到:
3291再次产生了3325(client2)
测试3:kill client1
此时我们发现了问题,client2没有重启client1!这是为什么呢(前面flock提到过)。后来查资料才发现,在进行fork时,父进程会把文件描述符也复制给子进程,
即使子进程调用了execlp但父进程复制的文件描述符仍然没有释放。即上文中复制的locfda。而对于flock文件锁来说,当所有的文件描述符被释放之后文件锁才被解开,所以问题就出现在这里!
经过查阅资料,我们可以添加文件声明以防止文件描述符的继承,在client1、client2代码段中加入如下代码(注释部分):
int flags = fcntl(lockfda,F_GETFD); flags |= FD_CLOEXEC; fcntl(lockfda,F_SETFD,flags);
具体原因可自行查阅,加入代码后测试正常:
到此处我们重点部分做完啦。
注意:如果想要停止运行代码,需要同时kill -9 两个进程号,后面代码同理。
五、完整代码测试在前面步骤的基础上,我加入了消息队列通信功能进行调试(消息队列代码参考华清远见-嵌入式ARM实验箱资料-I-FS4412-17-09-01内linux应用源码部分,当然自己写这个也不难,不过笔者又菜又懒)。加入后整体代码如下:
client1.c:
#include#include #include #include #include #include #include #include #define KEY_MSG 0x101 #define MSGSIZE 64 typedef struct { long mtype; char mtext[MSGSIZE]; } msgbuf; #define LEN sizeof(msgbuf) - sizeof(long) int main() { int msgid; int flag=0; msgbuf buf1, buf2; pid_t pid; msgid = msgget( KEY_MSG, 0666 ); int lockfda = open("locka", O_WRONLY); int lockfdb = open("lockb", O_WRONLY); int flags = fcntl(lockfda,F_GETFD); flags |= FD_CLOEXEC; fcntl(lockfda,F_SETFD,flags); if(flock(lockfda,LOCK_EX|LOCK_NB)==-1) { printf("time error:can't lock lockan"); exit(0); } while( 1 ) { if(flock(lockfdb,LOCK_EX|LOCK_NB)==-1) { printf("process client2 is runningn"); sleep(2); printf( "input the msg to client2:" ); strcpy(buf1.mtext," "); gets( buf1.mtext ); printf("gets:%sn", buf1.mtext); buf1.mtype = 1L; msgsnd( msgid, &buf1, LEN, 0 ); //客户端1获取消息并发往服务器 msgrcv( msgid, &buf2, LEN, 3L, 0 ); //准备从客户端2获取消息 if ( buf2.mtext[0] == 'x' || buf2.mtext[0] == 'X' ) { printf( "client1 will quit!n" ); break; } printf( "Receive from client2, message: %sn", buf2.mtext ); sleep(5); } else { flock(lockfdb, LOCK_UN); printf("process client2 is stopped and prepare to restart client2n"); sleep(3); pid=fork(); if(pid==0) { printf("start client2 successfullyn"); execlp("/home/linux/HW/experiments/multi_process/client2"," start-client2 ",NULL); } else { sleep(1); } } } return 0; }
client2.c:
#include#include #include #include #include #include #include #include #define KEY_MSG 0x101 #define MSGSIZE 64 #define KEY_MSG 0x101 #define MSGSIZE 64 typedef struct { long mtype; char mtext[MSGSIZE]; } msgbuf; #define LEN sizeof(msgbuf) - sizeof(long) int main() { int msgid; int flag=0; msgbuf buf1, buf2; msgid = msgget( KEY_MSG, 0666 ); pid_t pid; int lockfda = open("locka", O_WRONLY); int lockfdb = open("lockb", O_WRONLY); int flags = fcntl(lockfdb,F_GETFD); flags |= FD_CLOEXEC; fcntl(lockfdb,F_SETFD,flags); if(flock(lockfdb,LOCK_EX|LOCK_NB)==-1) { printf("time error:can't lock lockbn"); exit(0); } fflush(stdout); while( 1 ) { if(flock(lockfda,LOCK_EX|LOCK_NB)==-1) { flag=0; printf("process client1 is runningn"); sleep(2); msgrcv( msgid, &buf2, LEN, 4L, 0 ); //等待客户端1发消息 if( buf2.mtext[0] == 'x' || buf2.mtext[0] == 'X' ) { printf( "client2 will quit!n" ); break; } else printf( "Receive from client1, message: %sn", buf2.mtext ); sleep(1); //等待一秒,以确保客户端1已经收到了回执 printf( "input the msg to client1:" ); gets( buf1.mtext ); printf("gets:%sn", buf1.mtext); buf1.mtype = 2L; msgsnd( msgid, &buf1, LEN, 0 ); //给客户端1发送回执消息 sleep(5); } else { if(flag==1) { printf("process errorn"); exit(0); } flag=1; flock(lockfda, LOCK_UN); printf("process client1 is stopped and prepare to restart client1n"); sleep(3); pid=fork(); if(pid==0) { printf("start client1 successfullyn"); execlp("/home/linux/HW/experiments/multi_process/client1"," start-client1 ",NULL); } else { sleep(1); } } } }
server.c:
#include#include #include #include #include #include #include #define KEY_MSG 0x101 //使用共有的IPC key #define MSGSIZE 64 typedef struct //定义消息结构体:消息类型和消息数据 { long mtype; char mtext[MSGSIZE]; } msgbuf; #define LEN sizeof(msgbuf) - sizeof(long) int main() { int msgid; msgbuf buf1, buf2; msgid = msgget( KEY_MSG, IPC_CREAT|0666 ); while(1) { sleep(3); msgrcv(msgid, &buf1, LEN, 1L, 0); //接受客户端1的消息 printf( "Receive client1 message: %sn", buf1.mtext ); //打印收到的消息 if ( buf1.mtext[0] == 'x' || buf1.mtext[0] == 'X' ) //若是退出标志,则给2个客户端都发退出信息 { strcpy( buf1.mtext, "x" ); buf1.mtype = 3L; msgsnd( msgid, &buf1, LEN, 0 ); buf1.mtype = 4L; msgsnd( msgid, &buf1, LEN, 0 ); break; } buf1.mtype = 4L; msgsnd( msgid, &buf1, LEN, 0 ); //将客户端1的消息转发给客户端2 msgrcv( msgid, &buf2, LEN, 2L, 0 ); //接受客户端2的消息 printf( "Receive client2 message: %sn", buf2.mtext ); //打印收到的消息 if ( buf2.mtext[0] == 'x' || buf2.mtext[0] == 'X' ) //若是退出标志,则给2个客户端发退出信息 { strcpy( buf2.mtext, "x" ); buf2.mtype = 3L; msgsnd( msgid, &buf2, LEN, 0 ); buf2.mtype = 4L; msgsnd( msgid, &buf2, LEN, 0 ); break; } buf2.mtype = 3L; msgsnd( msgid, &buf2, LEN, 0 ); //将客户端2的消息转发给客户端1 fflush(stdin); fflush(stdout); sleep(10); } sleep(1); //若退出,则先等待,以确保客户端程序退出 msgctl( msgid, IPC_RMID, NULL ); //删除消息队列,释放空间 exit(0); }
将代码编译成可执行文件后,后台运行./server &,再运行./client1即可进行测试。
测试1:直接进行消息传输,正常
测试2:杀死client2,正常
注意:因为代码原因,必须进行一次完整消息传输后才能杀死某一个进程,因为消息队列函数是阻塞型的,如果没有收到消息将会阻塞在某个接收函数上,这个时候无法利用flock函数进行判断锁的状态导致无法重启。所以我在每一次循环处加入了延迟,测试者保证在这个时间内杀死某一个进程就好。
测试3:杀死client1,出现BUG
这个就出现令我不理解的地方了,也是我查询了很久也没发现的问题。杀死client1后,client1正常重启,但是会直接跳过get()函数,并且第一次发送到数据为v(此处为" ")是因为我将v用空格代替了,第二次数据为222(前一次传输的数据)。笔者尝试过fflush(stdin)、getchar()等多种清空输入缓冲区的办法,但仍然无法解决,也尝试过fget()、scanf(),因为实在找不出原因,也无效。个人觉得可能是fork或者消息队列函数的问题,但是耽误了很长时间仍然没有问题解决办法,若有读者发现问题所在,十分感谢能将您宝贵的意见发给我,谢谢!



