参考

UnixUnix环境高级编程 第三章 文件IO

偏移共享

单进程单文件描述符

在只有一个进程时,打开一个文件,对该文件描述符进行写入操作后,后续的写入操作会在原来偏移的基础上进行,这样就可以实现最一般的顺序写入了。

多进程单文件描述符

当多个进程共享一个描述符时他们的偏移也是共享的,比如在一个进行打开文件得到对应的描述符后,通过fork创建一个子进程,子进程进行的写入或者读取操作会影响父进程中文件描述符对应的偏移(因为其实修改的是同一个内核中的值)。

单进程多文件描述符

对于打开两个不相关的文件,对他们进行修改,那么他们的偏移也不会有相互的影响。然后考虑这样一种情况,同一进程内两次调用open打开了同一个文件分别得到了两个不同的文件描述符。此时对其中一个的偏移操作会不会影响到另一个?(也就是问:类Unix在进行open操作时会不会重用已经打开的文件描述符对象,使得通过open操作让多个index指向同一个文件描述符对象)

#include <stdio.h>
#include <string.h> #include <unistd.h>
#include <fcntl.h> int main() {
int fda = open("data00.txt", O_RDWR);
printf("fda = %d\n", fda);
int fdb = open("data00.txt", O_RDWR);
printf("fdb = %d\n", fdb); char buffer[256] = {0}; read(fda, buffer, 2); printf("offset fda : %ld\n", lseek(fda, 0, SEEK_CUR));
printf("offset fdb : %ld\n", lseek(fdb, 0, SEEK_CUR)); return 0;
}

编译运行后的输出如下:

fda = 3
fdb = 4
offset fda : 2
offset fdb : 0

可见两个文件描述符虽然指向同一个文件,但他们的偏移值是不相互影响的。这个其实是《Unix环境高级编程》中习题3.3的问题。

写入操作

当在一个进程内打开文件后,调用fork产生子进程,那么在子进程中也可以使用该文件描述符进行I/O。

并发直接写入

这里的自己写入表示不使用其他标志位或者函数改变文件描述符属性,而采用最一般的写入方式。由于fork后的进程共享同一描述符,他们所以他也就共享打开文件的当前偏移值。因而当两个进程各自直接调用write操作时他们其实是在向文件末尾不断写入数据。

#include <stdio.h>
#include <string.h> #include <unistd.h>
#include <fcntl.h> int main() {
umask(0);
mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; int fd = open("example.txt", O_CREAT | O_WRONLY, mode);
char buffer[256] = {0}; if (fork() > 0) {
strcpy(buffer, "parent-abc\n");
} else {
strcpy(buffer, "child#xyz#opqr\n");
} int i = 0;
for (i = 0; i<100000; i++) {
write(fd, buffer, strlen(buffer));
} close(fd);
return 0;
}

编译运行后进行统计:

sort example.txt | uniq -c
1
1 #xyz#opqr
1 arent-abc
1 bc
1 c
1 child#xparent-achild#xyz#opqr
3 child#xyz#oparechild#xparent-achiparent-abc
2 child#xyz#oparechild#xyz#opqr
3 child#xyz#oparent-abc
48749 child#xyz#opqr
1 child#xyz#opqrpchild#xyz#opqr
1 ild#xyz#opqr
1 opqr
2 parechild#xparent-achiparent-abc
3 parechild#xyz#opqr
55091 parent-abc
1 parent-child#xyz#opqr
1 qr
1 t-abc

可以发现采用原始的直接write输出并不能保证原子性,产生了许多碎片化的输出。

并发追加写入

追加写入对于日志一类的应用比较常见,如果此时两进程同时对一个描述符进行追加的write操作,会发生什么情况?写一个程序验证一下:

lseek

采用lseek+write的方式,因为这涉及到两个调用相比先前的直接写入方式多了一个,因而其原子性其实是不用期望了。

#include <stdio.h>
#include <string.h> #include <unistd.h>
#include <fcntl.h> int main() {
umask(0);
mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; int fd = open("example.txt", O_CREAT | O_WRONLY, mode);
char buffer[256] = {0}; if (fork() > 0) {
strcpy(buffer, "parent\n");
} else {
strcpy(buffer, "child\n");
} int i = 0;
for (i = 0; i<100000; i++) {
lseek(fd, 0, SEEK_END);
write(fd, buffer, strlen(buffer));
} close(fd);
return 0;
}

程序的功能为通过lseek调用将文件位置偏移设为文件结尾,每次在文件的最后附上一个输出字符串,其中父进程输出"parent",子进程输出"child"。编译运行,并对其输出文件进行如下统计:

$ sort example.txt | uniq -c
63423
99463 child
7 d
71 hild
27 ild
13 ld
3 nt
36233 parent
99 parentchild
3 parentcparenchild
1 parentcparenchparechiparchilpachildparent
1 parentcparenchparechiparchilpachildpchild
1 parentcparenchparechiparchilparent
7 parentcparenchparechiparent
13 parentcparenchparent
27 parentcparent
99 t

该统计给出了不同行的一个数量总和。可见一般的write+lseek并不是原子的。

O_APPEND选项

在打开文件时可以指定O_APPEND选项,表示是附加模式,即直接的一个write,其所写的数据肯定是附加在文件的末尾的。更进一步,不但对单个进程如此,对多个进程采用同一描述符进行write时也能保证这个性质。修改文件打开选项:

int fd = open("example.txt", O_CREAT | O_WRONLY | O_APPEND, mode);

此时我们再运行程序并统计得到正确的结果:

$ sort example.txt | uniq -c
100000 child
100000 parent

指定位置写入

如果要向指定位置写入数据比如在文件起始后10个byte的位置写入数据,可以使用两种方式:

  1. lseek + write
  2. pwrite

其实还可以将文件做内存映射不过这里不进行讨论了。lseek负责把打开文件描述对应的文件偏移设定到指定位置,然后write在该位置上开始输出。而pwrite则直接可以指定一个offset偏移值和数据进行输出,是前面两者的组合。

因为第一种方法将一个过程分散在两个调用内,不能保证是原子的。而后者即pwrite是可以保证写入的原子性的。

ssize_t pwrite(int fd, const void* buf, size_t nbytes, off_t offet);

由于pwrite只能根据绝对位置进行写入。对于追加操作,因为无法使用相对位置,pwrite也是无能为力的。

fcntl与状态类别

fcntl调用用于获取或修改打开文件、描述符的状态标识,这里的状态要分为两类

  1. 描述符状态标识
  2. 文件状态标识

描述符状态即只对该描述符起作用的,而文件状态则应用于文件,对于打开了该文件的所有进程都会产生影响。

描述符状态标识

FD_CLOEXEC代表该描述符是否在执行exec调用时被关闭(如果清除了该标记,则描述符对应的索引可以直接在exec启动的程序中使用,相当于做了一次重定向)

文件状态标识

这里特别要注意获取和修改个标记集合是不一样的,获取都可以,但只能修改某些标记。

获取F_GETFL

如以下一些通用的选项:

  1. O_RDONLY
  2. O_WRONLY
  3. O_RDWR
  4. O_APPEND
  5. O_NONBLOCK
  6. SYNC
  7. DSYNC
  8. RSYNC

修改F_SETFL

修改操作不能对以下几项进行修改:

  1. O_RDONLY
  2. O_WRONLY
  3. O_RDWR

作用域

按照《Unix环境高级编程》所述,

文件描述符标识是对于一个进程内的一个特定描述符来说的,而文件状态标识适用于指向该给定文件表项的任何进程中的所有描述符。

为了测试方便先定义一个获取文件状态标记的函数(主要考虑APPEND标记和其他一些读写标记):

char* get_mode(int fd, char* buffer) {
int pos = 0;
int mode = fcntl(fd, F_GETFL, 0); switch(mode & O_ACCMODE) {
case O_RDONLY:
pos += sprintf(buffer, "read only");
break;
case O_WRONLY:
pos += sprintf(buffer, "write only");
break;
case O_RDWR:
pos += sprintf(buffer, "read-write");
break;
default:
pos += sprintf(buffer, "error mode");
}
if (mode & O_APPEND) {
pos += sprintf(buffer + pos, ", append");
}
if (mode & O_NONBLOCK) {
pos += sprintf(buffer + pos, ", non-blocking");
}
if (mode & O_SYNC) {
pos += sprintf(buffer + pos, ", sync");
}
buffer[pos] = '\0';
return buffer;
}

单进程多描述符

在单进程内打开多个指向同一文件的描述符,然后通过一个调用fcntl修改标记然后再在两者上分别获取当前标记。

int main() {

	char buffer[256] = {0};

	int fda = open("data01.txt", O_RDONLY);
int fdb = open("data01.txt", O_RDWR); printf("pid = %u, mode: [%s]\n", getpid(), get_mode(fda, buffer));
printf("pid = %u, mode: [%s]\n", getpid(), get_mode(fdb, buffer)); int val = fcntl(fda, F_GETFL, 0);
int ret = fcntl(fda, F_SETFL, val | O_APPEND);
if (ret < 0) {
perror("fcntl");
return 0;
} printf("pid = %u, mode: [%s]\n", getpid(), get_mode(fda, buffer));
printf("pid = %u, mode: [%s]\n", getpid(), get_mode(fdb, buffer)); return 0;
}

运行输出为:

pid = 28253, mode: [read only]
pid = 28253, mode: [read-write]
pid = 28253, mode: [read only, append]
pid = 28253, mode: [read-write]

可以看到重新获取时只有fda的标记被改变,不过这里在单进程中的O_APPEND标记不易验证,换用另外一个标记O_SYNC即同步写入标记。不过令人惊讶的是fcntl居然对O_SYNC的修改没有效果即使是对本进程内同一个描述符。将程序修改为:

int main() {

	char buffer[256] = {0};

	int fda = open("data01.txt", O_RDWR);
int fdb = open("data01.txt", O_RDWR); printf("pid = %u, mode: [%s]\n", getpid(), get_mode(fda, buffer));
printf("pid = %u, mode: [%s]\n", getpid(), get_mode(fdb, buffer)); int val = fcntl(fda, F_GETFL, 0);
int ret = fcntl(fda, F_SETFL, val | O_SYNC);
if (ret < 0) {
perror("fcntl");
return 0;
} printf("pid = %u, mode: [%s]\n", getpid(), get_mode(fda, buffer));
printf("pid = %u, mode: [%s]\n", getpid(), get_mode(fdb, buffer)); struct timeval begin;
struct timeval end; char* str = "a very very long string is now ready to be writen in the file.\n";
int strn = strlen(str); gettimeofday(&begin, NULL); int i = 0; while (i++ < 10000) {
write(fda, str, strn);
}
gettimeofday(&end, NULL); printf("%ld us\n", (end.tv_sec - begin.tv_sec) * 1000000 + end.tv_usec - begin.tv_usec); return 0;
}

其输出为:

$ ./m4
pid = 28438, mode: [read-write]
pid = 28438, mode: [read-write]
pid = 28438, mode: [read-write]
pid = 28438, mode: [read-write]
28436 us

由于O_SYNC要求是写入同步的,因而在不到100ms内进行10000次IO显然是有悖常理的。于是将O_SYNC标记放到open调用参数中:

int fda = open("data01.txt", O_RDWR | O_SYNC);

此时程序运行时间明显变长输出为:

pid = 28445, mode: [read-write, sync]
pid = 28445, mode: [read-write]
pid = 28445, mode: [read-write, sync]
pid = 28445, mode: [read-write]
12093910 us

时长一下子变为了12s+,IOPS约为830,因为机器是15K的磁盘配有带Flash的RAID卡这个结果比较合理。如果我们把程序中的write(fda, str, strn)替换为write(fdb, str, strn)则其输出为:

pid = 28453, mode: [read-write, sync]
pid = 28453, mode: [read-write]
pid = 28453, mode: [read-write, sync]
pid = 28453, mode: [read-write]
28921 us

可见即使在同一个进程中两个描述符指向同一对象,他们在open时指定的O_SYNC标记并不是共享的,通过fcntl也无法进行修改。这个和《Unix环境高级编程》给出的说明是不符的,不知道问题出在哪里。

多进程同一描述符

这个场景可以通过fork来实现,fork前打开一个文件,然后父子进程中一方进行fcntl操作,另一方查看结果,看是否生效。由于现在有两个进程可以对O_APPEND进行验证。

int main() {

	char buffer[256] = {0};

	int fd = open("data01.txt", O_RDWR);

	printf("pid = %u, mode: [%s]\n", getpid(), get_mode(fd, buffer));

	if (fork() > 0) {
int val = fcntl(fd, F_GETFL, 0);
int ret = fcntl(fd, F_SETFL, val | O_APPEND | O_SYNC);
if (ret < 0) {
perror("fcntl");
return 0;
}
} else {
sleep(2);
} printf("pid = %u, mode: [%s]\n", getpid(), get_mode(fd, buffer)); return 0;
}

输出如下:

pid = 28512, mode: [read-write]
pid = 28512, mode: [read-write, append]
pid = 28513, mode: [read-write, append]

可见程序任然不能通过fcntl对O_SYNC标记位进行修改。其实这种情况和单个进程中的情况是非常类似的。

多进程同文件

这个和多进程同描述符不同,这里的两个进程没有父子关系,是完全两个不相关的进程。当他们打开文件并调用fcntl修改标记时是否会产生影响:

程序一:

int main() {

    char buffer[256] = {0};

    int fd = open("data01.txt", O_RDWR);

    printf("pid = %u, mode: [%s]\n", getpid(), get_mode(fd, buffer));

   int val = fcntl(fd, F_GETFL, 0);
int ret = fcntl(fd, F_SETFL, val | O_APPEND);
if (ret < 0) {
perror("fcntl");
return 0;
}
printf("pid = %u, mode: [%s]\n", getpid(), get_mode(fd, buffer)); sleep(1); char* str = "writer1-abc-edf\n";
int strn = strlen(str);
int i=0;
while (i++<100000) {
write(fd, str, strn);
} return 0;
}

程序二:

int main() {

	char buffer[256] = {0};

	int fd = open("data01.txt", O_WRONLY);

	printf("pid = %u, mode: [%s]\n", getpid(), get_mode(fd, buffer));

	sleep(1);

	printf("pid = %u, mode: [%s]\n", getpid(), get_mode(fd, buffer));

	int i=0;
char* str = "writer2-xyz-massive-things-hehe!\n";
int strn = strlen(str); while (i++ < 100000) {
write (fd, str, strn);
} return 0;
}

并编写一个脚本如下:

#! /bin/bash
echo "" > data01.txt
gcc mod_file1.c -o m1
gcc mod_file2.c -o m2
./m1 &
./m2 &

待程序输出后,执行脚本后进行统计:

pid = 28614, mode: [read-write]
pid = 28615, mode: [write only]
pid = 28614, mode: [read-write, append]
pid = 28615, mode: [write only] sort data01.txt | uniq -c
100000 writer2-xyz-massive-things-hehe!

可见O_APPEND并没有对两个进程序列化输出起到作用。如果对两个程序都加入O_APPEND标记则应该有如下统计结果:

sort data01.txt | uniq -c
1
1000000 writer1-abc-edf
1000000 writer2-xyz-massive-things-hehe!

不过也不能说O_APPEND选项没有起到作用,如果把两个O_APPEND全部取消则统计结果更乱:

sort data01.txt | uniq -c
1 ive-things-hehe!
998204 writer1-abc-edf
1 writer2-xywriter1-abc-edf
516021 writer2-xyz-massive-things-hehe!

但总的来说F_SETFL的修改并不能影响一个文件的多个不同文件描述符表项。

MISC

如果有以下程序:

#include <stdio.h>
#include <string.h> #include <unistd.h> int main() { char* msgs[] = {"apple\n", "juice\n"};
write(STDOUT_FILENO, msgs[0], strlen(msgs[0]));
write(STDERR_FILENO, msgs[1], strlen(msgs[1]));
return 0;
}

那么执行如下命令会有怎么样的不同和输出:

./a.out >outfile 2>&1
./a.out 2>&1 >outfile

结果:

$ ./a.out
apple
juice
$ ./a.out >outfile 2>&1
$ cat outfile
apple
juice
$ ./a.out 2>&1 >outfile
juice
$ cat outfile
apple

这是习题3.5,把一个>看成是一个dup2操作即可。

Unix环境高级编程:文件 IO 原子性 与 状态 共享的更多相关文章

  1. UNIX环境高级编程——文件I/O

    一.文件描述符 对于Linux而言,所有对设备或文件的操作都是通过文件描述符进行的.当打开或者创建一个文件的时候,内核向进程返回一个文件描述符(非负整数).后续对文件的操作只需通过该文件描述符,内核记 ...

  2. UNIX 环境高级编程 文件和目录

    函数stat , fstat , fstatat , lstat stat函数返回与此文件有关的信息结构. fstat函数使用已打开的文件描述符(而stat则使用文件名) fstatat函数 为一个相 ...

  3. UNIX环境高级编程——文件和目录

    一.获取文件/目录的属性信息 int stat(const char *path, struct stat *buf); int fstat(int fd, struct stat *buf); in ...

  4. UNIX环境高级编程 文件I/O

    大多数文件I/O 只需要用到 5个函数 :    open , read , write , lseek , close 本章描述的都是不带缓冲的I/O(read write 都调用内核中的一个系统调 ...

  5. UNIX环境高级编程 标准IO库

    标准I/O库处理很多细节,使得便于用户使用. 流和 FILE 对象 对于标准I/O库,操作是围绕 流(stream)进行的.当用标准I/O打开或创建一个文件时,我们已使一个流与一个文件相关联. 对于A ...

  6. UNIX环境高级编程---标准I/O库

    前言:我想大家学习C语言接触过的第一个函数应该是printf,但是我们真正理解它了吗?最近看Linux以及网络编程这块,我觉得I/O这块很难理解.以前从来没认识到Unix I/O和C标准库I/O函数压 ...

  7. (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  8. (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  9. (三) 一起学 Unix 环境高级编程 (APUE) 之 文件和目录

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

随机推荐

  1. Linux巩固记录(4) 运行hadoop 2.7.4自带demo程序验证环境

    本节主要使用hadoop自带的程序运行demo来确认环境是否正常 1.首先创建一个input.txt文件,里面任意输入些单词,有部分重复单词 2.将input文件拷贝到hdfs 3.执行hadoop程 ...

  2. 面试题-lazyMan实现

    原文:如何实现一个LazyMan 面试题目 实现一个LazyMan,可以按照以下方式调用: LazyMan('Hank'),输出: Hi, This is Hank! LazyMan('Hank'). ...

  3. Django模版结构优化和加载静态文件

    引入模版 有时候一些代码是在许多模版中都用到的.如果我们每次都重复的去拷贝代码那肯定不符合项目的规范.一般我们可以把这些重复性的代码抽取出来,就类似于Python中的函数一样,以后想要使用这些代码的时 ...

  4. oracle exp imp日常使用

    http://www.cnblogs.com/ningvsban/archive/2012/12/22/2829009.html http://www.cnblogs.com/mq0036/archi ...

  5. sql server 2012 复制数据库向导出现TransferDatabasesUsingSMOTransfer()异常

    Event Name: OnError Message: 传输数据时出错.有关详细信息,请参阅内部异常. StackTrace: 在 Microsoft.SqlServer.Management.Sm ...

  6. Java 内存分配及垃圾回收机制初探

    一.运行时内存分配 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域. 这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则 ...

  7. linux下实现进度条小程序

    转载自:实现一个简单的进度条 我们平常总会在下载东西或者安装软件的时候看到进度条,这里我们就在linux下实现这个进度条的功能. 1.我们使用的关键打印语句是printf函数: printf(&quo ...

  8. j2ee高级开发技术课程第十四周

    RPC(Remote Procedure Call Protocol) RPC使用C/S方式,采用http协议,发送请求到服务器,等待服务器返回结果.这个请求包括一个参数集和一个文本集,通常形成“cl ...

  9. postgresql 常用命令

    普通用法: sudo su - postgres 切换到postgres用户下: psql -U user -d dbname 连接数据库, 默认的用户和数据库是postgres \c dbname ...

  10. Go基础知识

    编程基础 Go程序是通过package来组织的(与Python类似) 只有package名称为main的包可以包含main函数 一个可执行程序有且仅有一个main包 一般结构basic_structu ...