引言:

本篇通过对open函数的讨论,引入原子操作,多进程通信(共享文件描述符)和内核相关的数据结构。

还会讨论集中常见的文件IO控制函数,包括:

  • dup和dup2
  • sync,fsync和fdatasync
  • fcntl
  • ioctl
  • /dev/fd

一、文件共享

这里所说的文件共享主要指的是进程间共享打开的文件。

这一节主要讨论文件在进程间共享的理论基础和数据结构,不涉及具体的技术实现,不同的系统可能会有不同的实现。

每一个打开的文件,涉及内核中的三种数据结构,这三种数据结构也是文件在进程间共享的基础。

  • an entry in the process table: 每一个打开的文件描述符对应一个entry,entry中的内容包括文件描述符标志位(file descriptor flags)和一个指向file table entry的指针;
  • file table:内核为所有打开的文件维护一个file table。每一个file table entry包括有:文件状态标志位(file status flag, such as read, write, append, sync和nonblocking)。
  • v-node和i-node: 每一个开打的文件都有一个v-node结构体,包括文件类型,指向操作函数的指针。对于大部分的文件,v-node还包含一个i-node结构。i-node的内容为打开文件时从硬盘上读取的信息,包括文件所有者,文件大小,文件内容存储在磁盘上的具体位置等。

下图表明了这三种内核数据结构的关系:

v-node是打开的文件在进程间共享的关键数据结构。如下图所示,两个进程打开同一个文件时的数据结构关系:

上图中,第一个进程打开文件,文件描述符为3, 第二进程打开同一个文件,文件描述符为4。

两个进程都有自己的file table entry,因为每个进程都需要维护自己的当前文件偏移量(current file offset)。

但是,也有可能多个独立进程的文件描述符指向同一个file table entry。这种情况发生在调用dup方法和fork系统调用时,父进程和子进程共享同一个file table entry。

我们还需要区分文件描述符标志位(file descriptor flag)和文件状态标记位(file status flag)。前者只在当前的进程的该文件描述符有效,而后者对于所有进程指向该file table entry的文件描述符都有效。这两个标志位的控制由函数fcntl控制。

上面我们所讨论的主要是多进程读同一个文件时所涉及的原理和数据结构,那么当多个进程同时写一个文件时,又是如何保证一致性呢?由此引出了原子操作(atomic operation)的概念。

二、原子操作(Atomic Operation)

老的版本的write函数并不支持O_APPEND标志。因此,追加写模式的实现如下代码所示:

 if (lseek(fd, 0L, ) < )       /* position to EOF */
err_sys(“lseek error");
if (write(fd, buf, ) != ) /* and write */
err_sys(“write error");

在单进程环境下,这段代码当然可以正常工作。

但是当在多进程环境下,由于进程切换的发生,并且各个进程独有的当前文件偏移量(存在file table entry中)并不会随时更新,在lseek和write调用中间,进程A被切换到另外一个进程B,进而往文件追加写了一部分数据,导致进程A的当前文件尾偏移量实效,当切换回进程A进行写时,覆盖了进程B所写的内容。

问题的原因在于,得到文件结尾处和写操作是由两个独立的函数调用完成的。

问题的解决方案是使得两个操作组成一个原子操作。

比较新的内核提供的O_APPEND标志位,可以让write每一次进行写操作前定位到文件尾,不需要单独调用lseek函数。

pread和pwrite函数

函数声明:

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

函数返回值和read、write函数相同。

调用pread相当于先调用lseek然后调用read,需要注意的两点是:

  • pread是原子操作;
  • 当前文件偏移量并不会被更新。

调用pwrite相当于原子性地先调用lseek,然后调用write。

三、常用的IO控制函数

1 dup和dup2函数

复制文件描述符。

函数声明:

 #inlcude <unistd.h>
int dup (int fd);
int dup2 (int fd, int fd2) 

返回值:

  • 非负整数:新的文件描述符,OK
  • -1:Error

功能说明:

  • dup:函数返回新的文件描述符,并且保证该描述符是最小可用的描述符。
  • dup2:使用fd2作为新的文件描述符。如果fd2已经被打开,则先关闭fd2。如果fd等于fd2,那么dup2返回不关闭fd2,直接返回fd2。

新旧文件描述符共享file table entry,如下图所示:

 newfd = dup();

假设下一个最小可用fd为3,则复制完成后:

新旧文件描述符共享file table entry, file status flag和current file offset。

每一个文件描述符都有自己的文件描述符标志位(file descriptor flag)。

新的文件描述符的标志位(file descriptor flag)会被dup函数清空。

另一种复制文件描述符的方式,使用fcntl:

dup(fd);    // ==
fcntl(fd, F_DUPFD, );
dup(fd, fd2); // ==
close(fd2);
fcntl(fd, F_DUPFD, fd2); 

2 sync, fsync, and fdatasync函数

先介绍一个机制:延迟写(delay write).

当磁盘发生IO时,系统内核会维护一个buffer cache或page cache。当我们向一个文件写数据时,数据先会被拷贝到内核的buffers中,在队列中等待写入到磁盘。这个机制叫延迟写(delay write)。

当需要使用buffer时,内核会把所有延迟写的block写回到磁盘中。为了保证磁盘和buffer中数据的一致性,Unix提供了函数sync, fsync和fdatasync。

函数声明:

 #include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
void sync(void);

功能说明:

sync:将buffer中所有被修改的块(block)放入队列中,等待写入,并立刻返回,它并不等待数据落盘。该函数往往周期性(常常为30s)地被调用。

fsync:根据fd指定单个文件,并且等待数据完全落盘才返回。

fdatasync:仅指定文件的数据部分,其他和fsync功能相似。

3 fcntl函数

fcntl函数用于修改已经打开文件的属性。

函数声明:

#include <fcntl.h>
int fcnt (int fd, int cmd, … /* int arg */);

功能说明:

函数fcntl有五种不同的功能:

  1. 复制一个已打开的文件描述符(cmd = F_DUPFD or F_DUPFD_CLOEXEC);
  2. 获取或设置文件描述符标志位(cmd = F_GETFD or F_SETFD);
  3. 获取或设置文件状态描述符(cmd = F_GETFL or F_SETFL);
  4. 获取或设置异步IO所属权(cmd = F_GETOWN or F_SETOWN);
  5. 获取或设置记录锁(cmd = F_GETLK, F_SETLK, or F_SETLKW);

在这里并不赘述这些cmd11个取值的具体含义,需要的时候可以自行查询。

函数返回值:

fcntl的返回值由cmd的取值决定,所有cmd均以返回-1为错误,其他值为OK。

有四个cmd的返回值需要注意:

  • F_DUPFD:返回新的文件描述符;
  • F_GETFD,F_GETFL:返回对应的flag;
  • F_GETOWN:返回一个活动进程号或者挂起状态进程组号(a positive process ID or a negative process group ID?)。

Example 01:

程序功能:返回制定文件描述符的权限状态。

源码:

 #include "apue.h"
#include <fcntl.h>
int
main(int argc, char *argv[])
{
int val;
if (argc != )
err_quit("usage: a.out <descriptor#>");
if ((val = fcntl(atoi(argv[]), F_GETFL, )) < )
err_sys("fcntl error for fd %d", atoi(argv[]));
switch (val & O_ACCMODE) {
case O_RDONLY:
printf("read only");
break;
case O_WRONLY:
printf("write only");
break;
case O_RDWR:
printf("read write");
break;
default:
err_dump("unknown access mode");
}
if (val & O_APPEND)
printf(", append");
if (val & O_NONBLOCK)
printf(", nonblocking");
if (val & O_SYNC)
printf(", synchronous writes"); #if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC)
if (val & O_FSYNC)
printf(", synchronous writes");
#endif putchar('\n');
exit();
}
测试:

第三条命令的执行结果和书上并不完全相同,系统之间的差异。

Example 02:

如果我们要修改文件描述符标志位(flag)或状态标志位(status flag),我们必须先获取文件描述符的当前状态值,按期望修改之后,重新为文件描述符标志位赋值。

源码:

 #include "apue.h"
#include <fcntl.h>
void
set_fl(int fd, int flags) /* flags are file status flags to turn on */
{
int val;
if ((val = fcntl(fd, F_GETFL, )) < )
err_sys("fcntl F_GETFL error");
val |= flags; /* turn on flags */
if (fcntl(fd, F_SETFL, val) < )
err_sys("fcntl F_SETFL error");

4 ioctl函数

ioctl函数是对所有IO操作函数的统称。在后面的章节会有更详细的说明,这里只是简要介绍一下。

函数声明:

 #include <unistd.h>    /*  System V */
#include <sys/ioctl.h> /* BSD and Linux */
int ioctl( int fd, int request, ... );

简单来说,ioctl负责那些难以用前一篇介绍的基本io操作(read, write, lseek等)来定义和实现的io操作。

后面的章节我们会遇到ioctl函数的使用场景,这里先略过。

5 /dev/fd

/dev/fd是一个文件夹,其中的文件为0,1,2 …,每一个文件都代表一个文件描述符。

打开(open)一个文件n,就相当于复制该文件描述符n,前提是该文件描述符已经被打开。

函数调用:

 fd = open(“/dev/fd/”, mode);

相当于:

 fd = dup();

再介绍一个命令行中使用的符号 “ - ”,代表标准输入(standard input)。这是一个很拙劣的设计,所以现在使用/dev/fd/0来代替。所以下面两条命令的作用是相同的。

 ls /dir/ | cat b.cpp -
ls /dir/ | cat b.cpp /dev/fd/ 

四、小结

这一篇主要介绍了几个常用的IO控制函数:

  • dup和dup2
  • sync、fsync和fdatasync
  • fcntl
  • iocntl

我们还讨论了多进程场景下的对同一个文件的读写和原子操作的概念。

这些知识点在后面的章节中还会遇到。

参考资料:

《Advanced Programming in the UNIX Envinronment 3rd》

UNIX高级环境编程(2)FIle I/O - 原子操作、共享文件描述符和I/O控制函数的更多相关文章

  1. UNIX高级环境编程1

    UNIX高级环境编程1 故宫角楼是很多摄影爱好者常去的地方,夕阳余辉下的故宫角楼平静而安详. 首先,了解一下进程的基本概念,进程在内存中布局和内容. 此外,还需要知道运行时是如何为动态数据结构(如链表 ...

  2. UNIX高级环境编程(1)File I/O

    引言: Unix系统中主要的文件操作包括: open read write lseek close unbuffered IO和standard I/O相对应,后面的章节我们会讨论这两者的区别. 在讨 ...

  3. UNIX高级环境编程(14)文件IO - O_DIRECT和O_SYNC详解 < 海棠花溪 >

    春天来了,除了工作学习,大家也要注意锻炼身体,多出去运动运动.  上周末在元大都遗址公园海棠花溪拍的海棠花.   进入正题. O_DIRECT和O_SYNC是系统调用open的flag参数.通过指定o ...

  4. UNIX高级环境编程(7)标准IO函数库 - 二进制文件IO,流定位,创建临时文件和内存流

    1 二进制IO(Binary IO) 在前一篇我们了解了逐字符读写和逐行读写函数. 如果我们在读写二进制文件,希望以此读写整个文件内容,这两个函数虽然可以实现,但是明显会很麻烦且多次循环明显效率很低. ...

  5. Unix高级环境编程

    [07] Unix进程环境==================================1. 进程终止    atexit()函数注册终止处理程序.    exit()或return语句:    ...

  6. UNIX高级环境编程(10)进程控制(Process Control)- 竞态条件,exec函数,解释器文件和system函数

    本篇主要介绍一下几个内容: 竞态条件(race condition) exec系函数 解释器文件    1 竞态条件(Race Condition) 竞态条件:当多个进程共同操作一个数据,并且结果依赖 ...

  7. UNIX高级环境编程(9)进程控制(Process Control)- fork,vfork,僵尸进程,wait和waitpid

    本章包含内容有: 创建新进程 程序执行(program execution) 进程终止(process termination) 进程的各种ID   1 进程标识符(Process Identifie ...

  8. UNIX高级环境编程(3)Files And Directories - stat函数,文件类型,和各种ID

    在前面的两篇,我们了解了IO操作的一些基本操作函数,包括open.read和write. 在本篇我们来学习一下文件系统的其他特性和一个文件的属性,涉及的函数功能包括: 查看文件的所有属性: 改变文件所 ...

  9. UNIX高级环境编程(12)进程关联(Process Relationships)- 终端登录过程 ,进程组,Session

    在前面的章节我们了解到,进程之间是有关联的: 每个进程都有一个父进程: 子进程退出时,父进程可以感知并且获取子进程的退出状态. 本章我们将了解: 进程组的更多细节: sessions的内容: logi ...

随机推荐

  1. tcpdump非常实用的抓包实例

    详细的文档见tcpdump高级过滤技巧 基本语法 ========过滤主机--------- 抓取所有经过 eth1,目的或源地址是 192.168.1.1 的网络数据# tcpdump -i eth ...

  2. mysql查看权限的命令

    mysql查看用户权限的命令 1.这里用来查看用户存储过程: show grants for 用户; eg: show grants for root@'localhost';#这样就会把root用户 ...

  3. 理解Spring定时任务的fixedRate和fixedDelay

    用过  Spring 的 @EnableScheduling 的都知道,我们用三种形式来部署计划任务,即 @Scheduled 注解的 fixedRate(fixedRateString), fixe ...

  4. gocommand:一个跨平台的golang命令行执行package

    最近在做一个项目的时候,需要使用golang来调用操作系统中的命令行,来执行shell命令或者直接调用第三方程序,这其中自然就用到了golang自带的exec.Command. 但是如果直接使用原生e ...

  5. HDU 1863 畅通工程(Prim算法求解MST)

    题目: 省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可).经过调查评估,得到的统计表中列出了有可能建设公路的若干条道路的成本.现 ...

  6. sgsdg

    wrjow we wetwer werwer werwer werqw qweqwrq qwrqwr @ApiOperation("根据条件分页查询试卷") @ApiRespons ...

  7. .net面试题[转载]

    1.简述private.protected.public.internal修饰符的访问权限. private:私有成员,在类的内部才可以访问. protected:保护成员,该类内部和继承类中可以访问 ...

  8. 打造自己的LinQProvider(四)

    打造自己的LinqProvider *:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: ...

  9. 对象的深度拓展$.extend(true,{},a,b),深入理解,小心陷阱

    转载:https://www.cnblogs.com/DJeanWeb/p/4388689.html $.extend一般情景下,使用深度拓展两个对象时,我们想要的效果是,b对象覆盖掉a对象中存在的所 ...

  10. [日常] Go语言圣经-WEB服务与习题

    Go语言圣经-web服务 1.Web服务程序,标准库里的方法已经帮我们完成了大量工作 2.main函数将所有发送到/路径下的请求和handler函数关联起来,/开头的请求其实就是所有发送到当前站点上的 ...