多进程下的文件写操作

背景:

进行多进程处理过程中,对处理结果写到结果文件中。

问题描述:

如果不采用锁或者其他进程同步的方式的话,如下代码:

void process_filelock_nonelock()
{

    int x=0;//如果操作的变量不是进程共享的话,那么就各自操作自己的x,不会相互影响。一个进程累计结果为20,另一个进程累计结果为40
    int rt;
    //尚未采用文件锁或者其他类型锁
    const char *lock_file="lock.test";
    std::ofstream res_file("test_file.txt");    
    rt=fork();//复制父进程,并创建子进程   
    if(rt==0)
    {   
        //子进程完成x+1
        for (int i=0;i<20;i++)
        {//加10次。相当于加10  

            x++;//
            printf("x++:%d\n",x);
            // res_file<<"x++:"<<x<<std::endl;//这种方式会有问题的,带缓存
            //以write的方式进行
            res_file.flush();//
            std::string tmp = "x++:"+Uint32ToString(x)+"\n";
            res_file.write(tmp.c_str(), tmp.size());//
            res_file.flush();//
            // close(fd);
            srand((unsigned)time(NULL));
            float a = rand() / (RAND_MAX + 1.0);
            sleep(a);
        }  
    }     
    else
    {   //父进程完成x+2

        for (int j=0;j<20;j++)
        {//加10次,相当于加20

            x+=2;
            printf("x+=2:%d\n",x);
            // res_file<<"x+=2:"<<x<<std::endl;//这种方式会有问题的,带缓存
            res_file.flush();//
            std::string tmp = "x+=2:"+Uint32ToString(x)+"\n";
            res_file.write(tmp.c_str(), tmp.size());//
            res_file.flush();//

            srand((unsigned)time(NULL));
            float a = rand() / (RAND_MAX + 1.0);
            sleep(a*1.2);
        }
    }
    return;
}

打印输出结果:

x+=2:2
x++:1
x+=2:4
x+=2:6
x+=2:8
x+=2:10
x+=2:12
x+=2:14
x+=2:16
x+=2:18
x+=2:20
x+=2:22
x+=2:24
x+=2:26
x+=2:28
x+=2:30
x+=2:32
x+=2:34
x+=2:36
x++:2
x+=2:38
x++:3
x+=2:40
x++:4
x++:5
x++:6
x++:7
x++:8
x++:9
x++:10
x++:11
x++:12
x++:13
x++:14
x++:15
x++:16
x++:17
x++:18
x++:19
x++:20

写入到结果文件的结果:

x+=2:2
x+=2:4
x+=2:6
x+=2:8
x+=2:10
x+=2:12
x+=2:14
x+=2:16
x+=2:18
x+=2:20
x+=2:22
x+=2:24
x+=2:26
x+=2:28
x+=2:30
x+=2:32
x+=2:34
x+=2:36
x++:2
x+=2:38
x+=2:40
x++:4
x++:5
x++:6
x++:7
x++:8
x++:9
x++:10
x++:11
x++:12
x++:13
x++:14
x++:15
x++:16
x++:17
x++:18
x++:19
x++:20

对比打印的结果和写到文件的结果,可以看出写到磁盘的结果数据缺失了两行。

这是因为写数据,涉及到两个操作,一个是write,一个是lseek。虽然两者各自都是原子操作,但是组合在一起完成一个写数据的操作时候,作为一个整体就不是原子性的。
例如进程或者线程A不在指向文件的真正末尾位置,它指向文件先前的默认位置,线程B写入信息的位置。当线程A写入文件时,可能被线程B写入的信息覆盖。这样就出现了上述的行缺失的情况。
我们可以通过加锁来实现。另外,对于被叠加的变量x,我们采用共享内存的方式,使得主进程和子进程可以分别对x进行累计,以展示另一种进程通信方式。

解决方案1:文件锁flock

void process_filelock_lock()
{

    int *x;
    int rt;  
    int shm_id;  
    const char *addnum="incdata";  
    void *ptr;  
    void *ptr1;  

    const char *lock_file="lock.test";//采用文件锁
    std::ofstream res_file("test_file.txt");

    shm_id=shm_open(addnum,O_RDWR|O_CREAT,0644);  
    ftruncate(shm_id,sizeof(int));//共享内存方式存放变量x的值

    rt=fork();//复制父进程,并创建子进程
    if(rt==0)
    {   
        //子进程完成x+1  
        ptr1=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*连接共享内存区*/  
        x=(int *)ptr1;

        for (int i=0;i<20;i++)
        {//加10次。相当于加10  

            int save_error;
            int fd = open(lock_file, O_TRUNC | O_CREAT);//创建记录锁
            if(-1 == flock( fd, LOCK_EX))//LOCK_NB,在尝试锁住该文件的时候,发现已经被其他服务锁住,会返回错误,errno错误码为EWOULDBLOCK。即提供两种工作模式:阻塞与非阻塞类型。
            {
                save_error = errno;//
                printf("Open fail with error %d\n", save_error);//未拿到锁
            }
            else
            {
                //拿到锁
                 (*x)++;
                printf("x++:%d\n",*x);
                res_file<<"x++:"<<*x<<std::endl;//这种方式会有问题的,带缓存
                close(fd);
                //也可以调用LOCK_UN参数来释放文件锁,flock(fd, LOCK_UN);

                srand((unsigned)time(NULL));
                float a = rand() / (RAND_MAX + 1.0);
                sleep(a);

            }

        }  
    }     
    else
    {   //父进程完成x+2  
        ptr=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*连接共享内存区*/  
        x=(int *)ptr;   

        for (int j=0;j<20;j++)
        {//加10次,相当于加20  
            int save_error;
            int fd = open(lock_file, O_TRUNC | O_CREAT);//创建记录锁
            if(-1 == flock( fd, LOCK_EX))//被其他进程占用了锁
            {
                save_error = errno;//
                printf("Open fail with error %d\n", save_error);//未拿到锁
            }
            else
            {
                (*x)+=2;  
                printf("x+=2:%d\n",*x);
                res_file<<"x+=2:"<<*x<<std::endl;//这种方式会有问题的,带缓存
                // std::string tmp="x+=2:" + Uint32ToString(*x)+"\n";
                close(fd);//解锁


                srand((unsigned)time(NULL));
                float a = rand() / (RAND_MAX + 1.0);
                sleep(a*1.2);
            }
        }

        //等待子进程
        int status =0;
        int mpid =0;
        mpid = wait(&status);
        printf("pid[%d] is exit with status[%d]\n",mpid,status);

    }

    close(shm_id);
    shm_unlink(addnum);//删除共享名称  
    munmap(ptr,sizeof(int));//删除共享内存  
    return;
}

经过多次运行,发现写到结果文件中的行数是40,并没有缺失,且最终的累加结果是60,与预期一致。
flock函数只能锁定整个文件,无法锁定文件的某一区域,而fcntl可以利用struct flock结构体,来实现文件里部分区域锁定的操作.

解决方案2:fcntl

函数原型:

int fcntl(int fd, int cmd);

int fcntl(int fd, int cmd, long arg);         

int fcntl(int fd, int cmd, struct flock *lock);

描述:

fcntl()针对(文件)描述符提供控制。参数fd是被参数cmd操作(如下面的描述)的描述符。

cmd选项参数:

F_DUPFD用来查找大于或等于参数arg的最小且仍未使用的文件描述词,并且复制参数fd的文件描述词。执行成功则返回新复制的文件描述词。请参考dup2()。F_GETFD取得close-on-exec旗标。若此旗标的FD_CLOEXEC位为0,代表在调用exec()相关函数时文件将不会关闭。
F_SETFD 设置close-on-exec 旗标。该旗标以参数argFD_CLOEXEC位决定。
F_GETFL 取得文件描述词状态旗标,此旗标为open()的参数flags。
F_SETFL 设置文件描述词状态旗标,参数arg为新旗标,但只允许O_APPEND、O_NONBLOCK和O_ASYNC位的改变,其他位的改变将不受影响。
F_GETLK 取得文件锁定的状态。
F_SETLK 设置文件锁定的状态。此时flcok 结构的l_type 值必须是F_RDLCK、F_WRLCK或F_UNLCK。如果无法建立锁定,则返回-1,错误代码为EACCES 或EAGAIN。
F_SETLKW F_SETLK 作用相同,但是无法建立锁定时,此调用会一直等到锁定动作成功为止。若在等待锁定的过程中被信号中断时,会立即返回-1,错误代码为EINTR。

fcntl函数有5种功能:

1.复制一个现有的描述符(cmd=F_DUPFD).
2.获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
3.获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
4.获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
5.获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
F_SETLK用来加锁、解锁;F_SETLKW功能同F_SETLK,只是操作变成阻塞式的

针对cmd的值,fcntl能够接受第三个参数(arg):

结构体flock的指针:

struct flcok

{

   short int l_type; /* 锁定的状态*/

    //这三个参数用于分段对文件加锁,若对整个文件加锁,则:l_whence=SEEK_SET,l_start=0,l_len=0;

   short int l_whence;/*决定l_start位置*/

   off_t l_start; /*锁定区域的开头位置*/

   off_t l_len; /*锁定区域的大小*/

   pid_t l_pid; /*锁定动作的进程*/

};

l_type 有三种状态:

   F_RDLCK 建立一个供读取用的锁定

   F_WRLCK 建立一个供写入用的锁定

     F_UNLCK 删除之前建立的锁定

l_whence 也有三种方式:

    SEEK_SET 以文件开头为锁定的起始位置。

    SEEK_CUR 以目前文件读写位置为锁定的起始位置

    SEEK_END 以文件结尾为锁定的起始位置。

具体使用例子:

void use_fcntl_lockfile()
{
    int *x;
    int rt;  
    int shm_id;  
    const char *addnum="incdata";
    void *ptr;  
    void *ptr1;  

    const char *lock_file="test_file.txt";//采用文件锁
    std::ofstream res_file(lock_file);

    shm_id=shm_open(addnum,O_RDWR|O_CREAT,0644);  
    ftruncate(shm_id,sizeof(int));//共享内存方式存放变量x的值
    int fd = open(lock_file,  O_RDWR | O_CREAT);//创建记录锁
    if(fd < 0)//LOCK_NB,在尝试锁住该文件的时候,发现已经被其他服务锁住,会返回错误,errno错误码为EWOULDBLOCK。即提供两种工作模式:阻塞与非阻塞类型。
    {
        int save_error;
        save_error = errno;//
        printf("Open fail with error %d\n", save_error);//未拿到锁
    }


    rt=fork();//复制父进程,并创建子进程
    if(rt==0)
    {   


        //子进程完成x+1  
        ptr1=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*连接共享内存区*/  
        x=(int *)ptr1;

        for (int i=0;i<20;i++)
        {//加10次。相当于加10  

            struct flock fcntl_lock;//
            fcntl_lock.l_type =  F_WRLCK;//建立一个供写入用的锁定
            fcntl_lock.l_whence = SEEK_SET;//以文件开头为锁定的起始位置
            fcntl_lock.l_start = 0;
            fcntl_lock.l_len = 0;//这两个参数表示整个文件锁定,暂时不是区域锁定
            int ret = fcntl( fd,F_SETLKW, &fcntl_lock );//获取锁,阻塞锁
            if(ret < 0)
            {
                printf("fcntl error,fail to lock\n");
                close(fd);
                return;//
            }
            //做处理
            //拿到锁
            (*x)++;
            printf("x++:%d\n",*x);
            std::string tmp = "x++:" + Uint32ToString(*x) + "\n";
            // res_file<<"x++:"<<*x<<std::endl;//这种方式会有问题的,带缓存
            (void)write(fd, tmp.c_str(), tmp.size());

            fcntl_lock.l_type = F_UNLCK;//解锁
            fcntl_lock.l_whence = SEEK_SET;
            fcntl_lock.l_start = 0;
            fcntl_lock.l_len = 0;
            int res = fcntl( fd,F_SETLKW,&fcntl_lock );//进行解锁
            if(res<0)
            {
                printf("child unlock fail\n");
            }
            // close(fd);

            srand((unsigned)time(NULL));
            float a = rand() / (RAND_MAX + 1.0);
            sleep(a);
        }  
    }     
    else
    {   

        //父进程完成x+2 
        ptr=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*连接共享内存区*/  
        x=(int *)ptr;

        for (int j=0;j<20;j++)
        {//加10次,相当于加20  
            struct flock fcntl_lock;//
            fcntl_lock.l_type =  F_WRLCK;//建立一个供写入用的锁定
            fcntl_lock.l_whence = SEEK_SET;//以文件开头为锁定的起始位置
            fcntl_lock.l_start = 0;
            fcntl_lock.l_len = 0;//这两个参数表示整个文件锁定,暂时不是区域锁定
            int ret = fcntl( fd,F_SETLKW, &fcntl_lock );//获取锁,锁定文件
            if(ret < 0)
            {
                printf("fcntl error,fail to lock\n");
                close(fd);
                return;
            }

            (*x)+=2;  
            printf("x+=2:%d\n",*x);
            // res_file<<"x+=2:"<<*x<<std::endl;//这种方式会有问题的,带缓存
            std::string tmp="x+=2:" + Uint32ToString(*x)+"\n";
            (void)write(fd, tmp.c_str(), tmp.size());

            fcntl_lock.l_type = F_UNLCK;//解锁
            fcntl_lock.l_whence = SEEK_SET;
            fcntl_lock.l_start = 0;
            fcntl_lock.l_len = 0;
            int res = fcntl( fd,F_SETLKW,&fcntl_lock );//进行解锁
            if(res<0)
            {
                printf("parent unlock fail\n");
            }


            srand((unsigned)time(NULL));
            float a = rand() / (RAND_MAX + 1.0);
            sleep(a*1.2);

        }

        //等待子进程
        int status =0;
        int mpid =0;
        mpid = wait(&status);
        printf("pid[%d] is exit with status[%d]\n",mpid,status);

    }

    close(fd);//解锁
    close(shm_id);
    shm_unlink(addnum);//删除共享名称  
    munmap(ptr,sizeof(int));//删除共享内存  
    return;
}

解决方案3:设置文件以append模式

open(filename,(FILE::WRONLY|FILE::APPEND))方式打开文件,这能够保证能lseekwrite两个操作发生的原子性。

void use_fcntl_lockfile()
{
    int *x;
    int rt;  
    int shm_id;  
    const char *addnum="incdata";
    void *ptr;  
    void *ptr1;  

    const char *lock_file="test_file.txt";//采用文件锁
    std::ofstream res_file(lock_file);

    shm_id=shm_open(addnum,O_RDWR|O_CREAT,0644);  
    ftruncate(shm_id,sizeof(int));//共享内存方式存放变量x的值

    rt=fork();//复制父进程,并创建子进程
    if(rt==0)
    {   
        //子进程完成x+1  
        ptr1=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*连接共享内存区*/  
        x=(int *)ptr1;

        for (int i=0;i<20;i++)
        {//加10次。相当于加10  

            int save_error;
            int fd = open(lock_file,  O_RDWR | O_CREAT | O_APPEND);
            if(fd < 0)
            {
                save_error = errno;//
                printf("Open fail with error %d\n", save_error);//未拿到锁
            }

            (*x)++;
            printf("x++:%d\n",*x);
            std::string tmp = "x++:" + Uint32ToString(*x) + "\n";
            // res_file<<"x++:"<<*x<<std::endl;//这种方式会有问题的,带缓存
            (void)write(fd, tmp.c_str(), tmp.size());

            srand((unsigned)time(NULL));
            float a = rand() / (RAND_MAX + 1.0);
            sleep(a);
        }  
    }     
    else
    {   
        ptr=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*连接共享内存区*/  
        x=(int *)ptr;   

        for (int j=0;j<20;j++)
        {//加10次,相当于加20  
            int save_error;
            int fd = open(lock_file, O_RDWR | O_CREAT | O_APPEND);//创建记录锁

            if(fd < 0)//LOCK_NB,在尝试锁住该文件的时候,发现已经被其他服务锁住,会返回错误,errno错误码为EWOULDBLOCK。即提供两种工作模式:阻塞与非阻塞类型。
            {
                save_error = errno;//
                printf("Open fail with error %d\n", save_error);//未拿到锁
            }

            (*x)+=2;  
            printf("x+=2:%d\n",*x);
            // res_file<<"x+=2:"<<*x<<std::endl;//这种方式会有问题的,带缓存
            std::string tmp="x+=2:" + Uint32ToString(*x)+"\n";
            (void)write(fd, tmp.c_str(), tmp.size());

            srand((unsigned)time(NULL));
            float a = rand() / (RAND_MAX + 1.0);
            sleep(a*1.2);

        }

        //等待子进程
        int status =0;
        int mpid =0;
        mpid = wait(&status);
        printf("pid[%d] is exit with status[%d]\n",mpid,status);

    }

    close(shm_id);
    shm_unlink(addnum);//删除共享名称  
    munmap(ptr,sizeof(int));//删除共享内存  
    return;
}

解决方案4:

pthread_mutex_t实现进程间的同步。
需要注意的是初始化mutex时需要指定PTHREAD_PROCESS_SHARED这个属性。
主要应用函数:

pthread_mutexattr_t mattr 类型:       用于定义mutex锁的属性
pthread_mutexattr_init函数:           初始化一个mutex属性对象
    int pthread_mutexattr_init(pthread_mutexattr_t*attr);
pthread_mutexattr_destroy函数:        销毁mutex属性对象 (而非销毁锁)
    int pthread_mutexattr_destroy(pthread_mutexattr_t*attr);
pthread_mutexattr_setpshared函数:     修改mutex属性。
    int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);

pshared的取值:
    线程锁:PTHREAD_PROCESS_PRIVATE(mutex的默认属性即为线程锁,进程间私有)
    进程锁:PTHREAD_PROCESS_SHARED

具体示例:

struct mt 
{
    pthread_mutex_t mutex;  
    pthread_mutexattr_t mutexattr;  
};

void test_pthread_mutex_lock()
{

    int *x;

    int rt;  
    int shm_id;  
    const char *addnum="myadd";  
    const char *mylock="mylock";
    void *ptr;  
    void *ptr1;
    struct mt *mm;



    std::ofstream res_file("test_file.txt");
    // pthread_mutex_t *mutex;//互斥对象,这个对象也是要放在共享内存中的,否则无法保证多个进程共用一份
    mm = (struct mt *)mmap(NULL, sizeof(*mm), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
    memset(mm, 0, sizeof(*mm));

    pthread_mutexattr_t mutexattr;//互斥对象属性  


    pthread_mutexattr_init(&mm->mutexattr);//初始化互斥对象属性  
    pthread_mutexattr_setpshared(&mm->mutexattr,PTHREAD_PROCESS_SHARED);//设置互斥对象为PTHREAD_PROCESS_SHARED共享,即可以在多个进程的线程访问,PTHREAD_PROCESS_PRIVATE为同一进程的线程共享  
    pthread_mutex_init(&mm->mutex, &mm->mutexattr);//初始化mutex琐


    shm_id=shm_open(addnum,O_RDWR|O_CREAT,0644);  
    ftruncate(shm_id,sizeof(int));      
    rt=fork();//复制父进程,并创建子进程   

    if(rt==0)
    {   
        //子进程完成x+1  
        // shm_id=shm_open(addnum,O_RDWR,0644);  
        ptr1=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*连接共享内存区*/  
        x=(int *)ptr1;    

        for (int i=0;i<20;i++)
        {//加10次。相当于加10
            pthread_mutex_lock(&mm->mutex);
            //拿到锁
            (*x)++;
            printf("x++:%d\n",*x);
            res_file<<"x++:"<<*x<<std::endl;//这种方式会有问题的,带缓存
            // std::string tmp1="x++:" + Uint32ToString(*x)+"\n";
            pthread_mutex_unlock(&mm->mutex);   
            srand((unsigned)time(NULL));
            float a = rand() / (RAND_MAX + 1.0);
            sleep(a);

        }  
    }     
    else
    {   //父进程完成x+2  
        ptr=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*连接共享内存区*/  
        x=(int *)ptr;   

        for (int j=0;j<20;j++)
        {//加10次,相当于加20  
            pthread_mutex_lock(&mm->mutex);//对共享内存中的数据加锁
            (*x)+=2;  
            printf("x+=2:%d\n",*x);
            res_file<<"x+=2:"<<*x<<std::endl;//这种方式会有问题的,带缓存
            std::string tmp="x+=2:" + Uint32ToString(*x)+"\n";
            pthread_mutex_unlock(&mm->mutex);  
            srand((unsigned)time(NULL));
            float a = rand() / (RAND_MAX + 1.0);
            sleep(a*1.2);
        }

    }
    close(shm_id);

    shm_unlink(addnum);//删除共享名称  
    pthread_mutexattr_destroy(&mm->mutexattr);//销毁mutex属性对象
    pthread_mutex_destroy(&mm->mutex);//销毁mutex
    munmap(ptr,sizeof(int));//删除共享内存  
    return;
}

多次运行结果可以看出,写到结果文件的行数保持40行,且累计结果为60.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值