linux多线程编程中IO读写的安全函数,pread/pwrite和read/write有什么区别和联系,实例代码对比

preadpwrite 函数是 linux 下 C 语言编程中非常好用的 IO 操作函数。它们属于系统调用,在 2.1.60 之后版本的 linux 下都可以使用,尤其适合用于多线程的应用中,它们允许多个线程操作同一个文件描述符,不会互相影响彼此的文件偏移(offset)。

pread 和 pwrite 函数

所需头文件和函数原型

#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

函数说明

preadpwrite函数从文件描述符 fd(可由open函数获得)描述的文件的 offset 偏移处,读写 count 字节数据。

如果是读,读出的内容通过buf传出。如果是写,写入的内容由buf传入。

它俩的返回值与readwrite函数类似,成功时,preadpwrite函数分别返回成功读出或者写入的字节数。如果调用失败,返回 -1,失败码可从errno查出。

pread/pwrite 和 read/write 函数的联系和区别

以写入函数为例,pwrite函数在单线程的情况下,相当于 lseekwrite函数的组合:

pwrite(fd, buf, len, offset);
/**   相当于   */
lseek(fd, offset, SEEK_SET);
write(fd, buf, len);

只不过,在 fd文件描述符没有O_APPEND属性的情况下,write函数会修改文件偏移指针,下次不seek写入位置直接调用write函数时,write函数会接着上次的位置继续写入(offset+len)。而pwrite函数则不会改变文件偏移指针。

另外,很重要的一点是,pwrite函数的seek写入是原子操作,不会因为进程调度或者其他因素中断。而对于lseekwrite的组合,因为两个语句之间存在间隙,进程调度线程切换有可能发生在其间,导致写入的数据出错。

pread/pwrite 和 read/write 函数的实例demo对比

下面这段代码的功能很简单,就是创建了两个线程,一个线程负责顺序往 test.bin 文件顺序写入 0~254 的整数,另外一个线程则负责从中顺序读出。编译后,程序接收一个参数,1表示只从test.bin中读取,2表示只往test.bin中写入,3表示同时读写test.bin

lseekread/write的组合在多线程同时读写中的表现

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>

int fd;
#define        MAX         255
#define        filename    "test.bin"

void* r_thread(void* p)
{
    int i = 0, j = 0;

    for(i=0; i<MAX; i++){
        usleep(100);

        lseek(fd, i, SEEK_SET);
        read(fd, &j, 1);
        // pread(fd, &j, 1, i);

        printf("%s: %d\n", __FUNCTION__, j);
    }

    return NULL;
}

void* w_thread(void* p)
{
    int i = 0, j = 0;

    for(i=0; i<MAX; i++){
        usleep(100);

        lseek(fd, i, SEEK_SET);
        write(fd, &i, 1);
        // pwrite(fd, &j, 1, i);

        printf("%s: %d\n", __FUNCTION__, j);
    }

    return NULL;
}

int main(int argc, char* argv[])
{
    pthread_t ppid;

    fd = open(filename, O_RDWR|O_CREAT, 0777);

    if(argc<2){
        printf("\n  usage: %s [1(read),2(write),3(both read and write)]\n\n", argv[0]);
        exit(1);
    }

    switch(atoi(argv[1])){
        case 1:
            pthread_create(&ppid, NULL, r_thread, NULL);
            break;
        case 2:
            pthread_create(&ppid, NULL, w_thread, NULL);
            break;
        case 3:
            pthread_create(&ppid, NULL, r_thread, NULL);
            pthread_create(&ppid, NULL, w_thread, NULL);
            break;
        default:
            printf("\n  usage: %s [1(read),2(write),3(both read and write)]\n\n", argv[0]);
            break;
    }

    getchar();
    return 0;
}

我们编译,执行,发现有些数字并没有被顺序写入。

$ gcc pread_vs_read.c -lpthread
$ ./a.out 3
w_thread: 0
r_thread: 0
r_thread: 254
...
$ ./a.out 1
...
r_thread: 43
r_thread: 211
r_thread: 210
r_thread: 46
r_thread: 208
r_thread: 48
r_thread: 52
r_thread: 202
r_thread: 54
...

原因上面已经分析,就是因为lseekread/write的组合并不是原子操作,而且read/write函数也会改变文件偏移指针,这都会导致多线程同时读写时的数据紊乱。

pread/pwrite在多线程同时读写中的表现

现在来试试pread/pwrite函数的表现如何,我们将代码中的lseekread/write注释,取消pread/pwrite的注释:

...
// lseek(fd, i, SEEK_SET);
// read(fd, &j, 1);
pread(fd, &j, 1, i);
...
// lseek(fd, i, SEEK_SET);
// write(fd, &i, 1);
pwrite(fd, &i, 1, i);
...

再编译,做相同的测试:

$ gcc pread_vs_read.c -lpthread
lcc@lcc-vm:/lccRoot/xx_test/tmp/pread_vs_read$ ./a.out 3
w_thread: 0
r_thread: 0
w_thread: 1
r_thread: 1
...
$ ./a.out 1
r_thread: 0
r_thread: 1
r_thread: 2
r_thread: 3
r_thread: 4
r_thread: 5
...

发现数据被正确写入了。

总结

可以看出,pread/pwrite更适合在多线程编程中使用。当然,非要用read/write也可以,只是需要自行做好线程同步,这就比pread/pwrite麻烦了许多。

阅读更多:   C语言 , Linux笔记
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif