栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C语言:基于文件锁实现进程间的相互启动与通信(基础)

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C语言:基于文件锁实现进程间的相互启动与通信(基础)

一、写在前面

学生党一枚,很多概念都处于一知半解的状态,理解不是很清晰,代码存在一些bug,文章中也进行了详细叙述。这是第一次发博文,排版可能不是很好看,抱以记录及以学习态度,如果文章有任何不对的地方,欢迎指正,十分感谢!

二、项目介绍

在linux系统下实现两位用户端利用flock文件锁相互对对方状态(死亡、存活)进行监听,当一方死亡后另外一方用fork及execlp函数实现复苏。当两位用户都处于存活状态下时利用消息队列进行通信。文章涉及知识点包括:flock文件锁使用、fork与execlp函数的联合使用、消息队列通信(不作叙述,需要自行了解)。

文件目录:

我们需要解决的关键性问题如下:如何启动一个进程、如何知道一个进程的状态(死亡、存活)。

三、基础知识介绍以及程序框架梳理 1.fork函数

      fork函数用于产生一进程(父进程)的副本进程(子进程),执行fork函数后,子进程会包含父进程的所有内容,包括数据空间、执行位置等。子进程和父进程的区分通过fork()返回值来确定,在子进程中,fork函数返回0,父进程中返回子进程的PID。因为复制了所有内容,所以我们只需要加上一个简单if(pid==   )的判断就可以分别在父进程与子进程中写入代码。fork函数基本结构如下:

#include 
#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;

}

2.execlp函数

  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或者消息队列函数的问题,但是耽误了很长时间仍然没有问题解决办法,若有读者发现问题所在,十分感谢能将您宝贵的意见发给我,谢谢!

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/847881.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号