UNIX高级环境编程(2)FIle I/O - 原子操作、共享文件描述符和I/O控制函数
引言:
本篇通过对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标志。因此,追加写模式的实现如下代码所示:
1 if (lseek(fd, 0L, 2) < 0) /* position to EOF */ 2 err_sys(“lseek error"); 3 if (write(fd, buf, 100) != 100) /* and write */ 4 err_sys(“write error");
在单进程环境下,这段代码当然可以正常工作。
但是当在多进程环境下,由于进程切换的发生,并且各个进程独有的当前文件偏移量(存在file table entry中)并不会随时更新,在lseek和write调用中间,进程A被切换到另外一个进程B,进而往文件追加写了一部分数据,导致进程A的当前文件尾偏移量实效,当切换回进程A进行写时,覆盖了进程B所写的内容。
问题的原因在于,得到文件结尾处和写操作是由两个独立的函数调用完成的。
问题的解决方案是使得两个操作组成一个原子操作。
比较新的内核提供的O_APPEND标志位,可以让write每一次进行写操作前定位到文件尾,不需要单独调用lseek函数。
pread和pwrite函数
函数声明:
1 #include <unistd.h> 2 ssize_t pread (int fd, void *buf, size_t nbytes, off_t offset); 3 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函数
复制文件描述符。
函数声明:
1 #inlcude <unistd.h> 2 int dup (int fd); 3 int dup2 (int fd, int fd2)
返回值:
- 非负整数:新的文件描述符,OK
- -1:Error
功能说明:
- dup:函数返回新的文件描述符,并且保证该描述符是最小可用的描述符。
- dup2:使用fd2作为新的文件描述符。如果fd2已经被打开,则先关闭fd2。如果fd等于fd2,那么dup2返回不关闭fd2,直接返回fd2。
新旧文件描述符共享file table entry,如下图所示:
1 newfd = dup(1);
假设下一个最小可用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, 0); 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。
函数声明:
1 #include <unistd.h> 2 int fsync(int fd); 3 int fdatasync(int fd); 4 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有五种不同的功能:
- 复制一个已打开的文件描述符(cmd = F_DUPFD or F_DUPFD_CLOEXEC);
- 获取或设置文件描述符标志位(cmd = F_GETFD or F_SETFD);
- 获取或设置文件状态描述符(cmd = F_GETFL or F_SETFL);
- 获取或设置异步IO所属权(cmd = F_GETOWN or F_SETOWN);
- 获取或设置记录锁(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:
程序功能:返回制定文件描述符的权限状态。
源码:
1 #include "apue.h" 2 #include <fcntl.h> 3 int 4 main(int argc, char *argv[]) 5 { 6 int val; 7 if (argc != 2) 8 err_quit("usage: a.out <descriptor#>"); 9 if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0) 10 err_sys("fcntl error for fd %d", atoi(argv[1])); 11 switch (val & O_ACCMODE) { 12 case O_RDONLY: 13 printf("read only"); 14 break; 15 case O_WRONLY: 16 printf("write only"); 17 break; 18 case O_RDWR: 19 printf("read write"); 20 break; 21 default: 22 err_dump("unknown access mode"); 23 } 24 if (val & O_APPEND) 25 printf(", append"); 26 if (val & O_NONBLOCK) 27 printf(", nonblocking"); 28 if (val & O_SYNC) 29 printf(", synchronous writes"); 30 31 #if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC) 32 if (val & O_FSYNC) 33 printf(", synchronous writes"); 34 #endif 35 36 putchar('\n'); 37 exit(0); 38 }
第三条命令的执行结果和书上并不完全相同,系统之间的差异。
Example 02:
如果我们要修改文件描述符标志位(flag)或状态标志位(status flag),我们必须先获取文件描述符的当前状态值,按期望修改之后,重新为文件描述符标志位赋值。
源码:
1 #include "apue.h" 2 #include <fcntl.h> 3 void 4 set_fl(int fd, int flags) /* flags are file status flags to turn on */ 5 { 6 int val; 7 if ((val = fcntl(fd, F_GETFL, 0)) < 0) 8 err_sys("fcntl F_GETFL error"); 9 val |= flags; /* turn on flags */ 10 if (fcntl(fd, F_SETFL, val) < 0) 11 err_sys("fcntl F_SETFL error"); 12 }
4 ioctl函数
ioctl函数是对所有IO操作函数的统称。在后面的章节会有更详细的说明,这里只是简要介绍一下。
函数声明:
1 #include <unistd.h> /* System V */ 2 #include <sys/ioctl.h> /* BSD and Linux */ 3 int ioctl( int fd, int request, ... );
简单来说,ioctl负责那些难以用前一篇介绍的基本io操作(read, write, lseek等)来定义和实现的io操作。
后面的章节我们会遇到ioctl函数的使用场景,这里先略过。
5 /dev/fd
/dev/fd是一个文件夹,其中的文件为0,1,2 …,每一个文件都代表一个文件描述符。
打开(open)一个文件n,就相当于复制该文件描述符n,前提是该文件描述符已经被打开。
函数调用:
1 fd = open(“/dev/fd/0”, mode);
相当于:
1 fd = dup(0);
再介绍一个命令行中使用的符号 “ - ”,代表标准输入(standard input)。这是一个很拙劣的设计,所以现在使用/dev/fd/0来代替。所以下面两条命令的作用是相同的。
1 ls /dir/ | cat b.cpp - 2 ls /dir/ | cat b.cpp /dev/fd/0
四、小结
这一篇主要介绍了几个常用的IO控制函数:
- dup和dup2
- sync、fsync和fdatasync
- fcntl
- iocntl
我们还讨论了多进程场景下的对同一个文件的读写和原子操作的概念。
这些知识点在后面的章节中还会遇到。
参考资料:
《Advanced Programming in the UNIX Envinronment 3rd》
转载于:https://www.cnblogs.com/suzhou/p/4293565.html
UNIX高级环境编程(2)FIle I/O - 原子操作、共享文件描述符和I/O控制函数相关推荐
- 《UNIX高级环境编程》 -- apue.h
在看<UNIX高级环境编程>这本书的时候,会遇到一个问题就是这个"apue.h",这个是作者为了编写代码方便封装了一个库,我们可以使用下面的方式解决这个问题,让我们的代 ...
- Unix高级环境编程 学习小结(一)
这个系列的博客主要是针对Unix高级环境编程的内容做一个小结,针对的是面试中经常问道或者可能问到的问题. Unix体系结构: 环境的核心是内核,内核的接口被称为系统调用,公用函数库建立在系统调用接口只 ...
- Unix高级环境编程
[07] Unix进程环境 ================================== 1. 进程终止 atexit()函数注册终止处理程序. exit()或return语句 ...
- UNIX高级环境编程(9)进程控制(Process Control)- fork,vfork,僵尸进程,wait和waitpid...
本章包含内容有: 创建新进程 程序执行(program execution) 进程终止(process termination) 进程的各种ID 1 进程标识符(Process Identifiers ...
- UNIX高级环境编程—第八章进程控制
第八章-进程控制 1进程相关概念 1.1 程序和进程 1.2 并行和并发 1.3 进程控制块 1.4 进程标志 1.5进程状态(面试考) 2 创建进程 2.1 fork函数 2.2 ps命令和kill ...
- unix高级环境编程-基础知识
对于一个从C#转向Linux/unix系统开发的菜鸟,中间无数次的焦头烂额只有自己心理明白,所幸我没有在大风大浪中凋零破碎,依然坚挺着我的学习之路. Unix操作系统结构: UNIX ,是一个强大的多 ...
- Unix高级环境编程—进程控制(一)
一.函数fork #include<unistd.h> pid_t fork(void) ...
- UNIX高级环境编程 第3章 文件IO
3.3 函数open和openat open int open(const char *pathname, int flags);int open(const char *pathname, int ...
- UNIX高级环境编程 第11、12章 线程同步及属性
第11.12章 线程及其控制 主要内容 互斥量 非递归互斥量 递归互斥量 读写锁 条件变量 自旋锁 屏障 互斥量 int pthread_mutex_init(pthread_mutex_t *res ...
- 外网访问arm嵌入式linux_嵌入式Linux系统编程——文件读写访问、属性、描述符、API
Linux 的文件模型是从 Unix 的继承而来,所以 Linux 继承了 UNIX 本身的大部分特性,然后加以扩展,本章从 UNIX 系统接口来描述 Linux 系统结构的特性. 操作系统是通过一系 ...
最新文章
- win7完美兼容DynamipsGUI(小凡模拟器)攻略
- 因果解释能够对规则进行解释吗?
- Android Git 客户端
- C++ map基本操作
- 用SQL语句添加删除修改字段_常用SQL
- MacBook/MacOS/Mac OS 查看进程/端口信息的相关命令
- 初学必读:61条面向对象设计的经验原则
- 黑客们的故事(连载三) 因为好奇
- 2021上半年朋友圈都在传的10本书,找到了
- Android开发笔记(一百五十三)OpenGL绘制三维图形的流程
- ftp 批量上传文件命令
- 整理vim格式回车变成空两格
- MySql is marked as crashed and should be repaired问题
- QGIS教程02---QGIS加载数据的4种方法
- 壳的概念、LordPE的使用、C#读取PE文件初步
- Keil_v5 下生成bin文件+Jflash 下载bin文件
- 计算机工作多少,一台普通计算机能工作多少电量?
- 红米S2解BL锁教程申请BootLoader解锁教程
- 程序员成长之旅——同步IO和异步IO(五种IO模型)
- Outlook邮箱无法登录
热门文章
- 阶段3 2.Spring_06.Spring的新注解_8 spring整合junit完成
- 阶段3 2.Spring_03.Spring的 IOC 和 DI_5 BeanFactory和ApplicationContext的区别
- 阶段1 语言基础+高级_1-3-Java语言高级_06-File类与IO流_02 递归_3_练习_使用递归计算阶乘...
- BZOJ1815 SHOI2006有色图(Polya定理)
- laravel 分页使用
- BootStrap中常用样式类
- 【华为机试】—— 12字符串反转
- 2017-4-24(1493037086057未命名文件 测试资源是否正确上传
- 基于XML的AOP配置-转
- .net中的lock