Unix环境高级编程:文件 IO 原子性 与 状态 共享

参考

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操作即可。

posted @ 2015-08-02 10:02  卖程序的小歪  阅读(431)  评论(0编辑  收藏  举报