本章开始讨论UNIX系统的文件I/O函数,包括打开文件、读文件、写文件等。

UNIX系统中的大多数文件I/O只需要用到5个函数:open、read、write、lseek和close。它们每执行一次都会调用内核中的系统调用,也就是常说的不带缓冲的I/O函数

只要涉及多个进程间共享资源(比如同时读写某个文件),原子操作的概念就变得很重要,我将通过open()函数来讨论此概念。


一、文件描述符

文件描述符是一个非负整数,每一个使用open()函数打开的文件都会分配一个文件描述符

在第一章提到的标准输入、标准输出、标准错误分别对应文件描述符0、1、2

每个操作系统中的文件描述符个数都是一定的,我们可以使用如下shell命令查看最大文件描述符数值:

  $ ulimit -n

也可使用下面的代码进行查看:

 #include <unistd.h>

 sysconf(_SC_OPEN_MAX)

二、文件操作函数

UNIX系统中的大多数文件I/O只需要用到5个函数:open、read、write、lseek和close。我在下面给出它们的函数声明、例子和函数作用:

 /* 文件打开函数 */
#include <fcntl.h> int open(const char *path, int oflag, ... /* mode_t mode */ );
int openat(int fd, const char *path, int oflag, ... /* mode_t mode */ ); /* 例子 */
int fd = open("a.txt", O_RDWD | O_CREAT, );
if (fd == -) /* 失败 */
return -;

函数参数以及返回值:

path:需要打开文件的路径

oflag:打开文件的权限(比如是否可以读、是否可以写等)

mode:创建文件(O_CREAT)的权限,0666表示用户权限、组权限和其他人权限,我将在下面介绍

返回值:成功返回文件描述符;出错返回-1


关于oflag的定义一般有以下几类,不同类之间可以如上面例子,使用“|”组合。常用的我会使用加粗字体。

O_RDONLY、O_WRONLY、O_RDWR、O_EXEC:只读、只写、既可以读也可以写、只执行

O_APPEND追加到文件的尾部

O_CREAT如果没有名为path的文件,就会创建此文件

O_TRUNC:如果此文件存在,而且以只写或读写打开,将其长度截断为0,也就是删除文件内容

在我们使用shell命令ls -al时,会显示文件的权限,如:

drwxr-xr-x  2 lioker lioker  4096 7月   6  2017 Music

drwxr-xr-x其中的d表示目录,d后面的rwx表示用户权限为7(rwx分别用二进制表示,如果有权限为1,没有权限为0);rwx后面的r-x表示组中没有写权限,为5,其他人权限与组权限相同。

 /* 文件读写函数 */
#include <unistd.h> ssize_t read(int fd, void *buf, size_t nbytes);
ssize_t write(int fd, const void* buf, size_t n); /* 例子 */
int res; /* 用来接收读写文件的返回值 */
int buf[] = {, , };
int rcv; res = write(fd, buf, * ); /* 3为个数,4为字节数 */
printf("Write Bytes: %d\n", res); res = read(fd, &rcv, ); /* buf为World World */
printf("Read Bytes: %d Content: %d\n", res, rcv);

函数参数以及返回值:

fd:文件打开函数返回的文件描述符

buf:读/写变量数据

nbytes:需要读的字节数

n:需要写的字节数

返回值:成功返回实际读写的字节数;出错返回-1


如果读者执行上面两行代码,会发现read函数返回的是写入的值。

这是由于文件有一个与其关联的“文件偏移量”,用于表示当前文件读/写到哪里。这个偏移量我们可以使用lseek函数控制。

还有一点需要读者注意:

对于read和write函数,一定要注意其操作的是内存中的字节数,比如要用read和write去读写int类型变量,则一次性要读写32位,也就是4字节。因此其是二进制还是文本模式取决于对字节的解释。具体请查看上面的例子。

 /* 文件偏移量控制 */
#include <unistd.h> off_t lseek(int fd, off_t offset, int whence); /* 例子 */
lseek(fd, 0, SEEK_SET);

函数参数以及返回值:

fd:文件打开函数返回的文件描述符

offset:偏移量

whence:偏移量设置方式

返回值:成功返回新的文件偏移量;出错返回-1


关于whence有以下几类(下面的offset就是lseek()的第二个参数数值):

SEEK_SET:从文件开始处开始偏移offset

SEEK_CUR:从当前位置处开始偏移offset

SEEK_END:从文件结尾处开始偏移offset

 /* 文件关闭函数 */
#include <unistd.h> int close(int fd); /* 例子 */
close(fd);

函数参数以及返回值:

fd:文件打开函数返回的文件描述符

返回值:成功返回0;出错返回-1


三、文件操作示例

 #include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h> #define BUFSIZE 4096 int main()
{
int fd = open("a.txt", O_RDWR | O_CREAT | O_TRUNC, ); int res; /* 用来接收读写文件的返回值 */ char snd = 'A';
int i;
char buf[BUFSIZE] = {}; #if 1
for (i = ; i < BUFSIZE - ; ++i) {
buf[i] = snd;
} res = write(fd, buf, strlen(buf));
printf("Write Bytes: %d\n", res);
#else
for (i = ; i < BUFSIZE; ++i) {
write(fd, &snd, );
}
#endif lseek(fd, , SEEK_SET); res = read(fd, buf, BUFSIZE);
printf("Read Bytes: %d Content: %s\n", res, buf); return ;
}

四、I/O的效率

由于read()和write()是不带缓冲的,因此每一次的调用都会进行一次内核调用,这会对I/O的效率造成很大的影响。

比如把第三节程序的第18行#if 1改为0,然后在命令行编译后使用,可以发现效率有明显的降低:

time ./a.out

一般在使用时,我们会定义一个缓存区用于存储数据,等到数据满了之后,再调用read()和write()。

五、原子操作

原子操作指的是一系列的操作是密不可分的,要么完成全部,要么一个都没完成,是不可能只执行了其中的一部分的。

六、dup()和dup2()

dup()和dup2()用来复制一个现有的文件描述符。函数声明如下:

 /* 复制文件描述符函数 */
#include <unistd.h> int dup(int fd);
int dup2(int fd, int fd2); /* 例子 */
int new = dup(fd);
printf("Dup New Fd: %d\n", new); int new = dup2(fd, new);
printf("Dup New Fd: %d\n", new);

函数参数以及返回值:

fd:文件打开函数返回的文件描述符

fd2:需要fd变换为的文件描述符

返回值:成功返回新的文件描述符;出错返回-1。对于dup()来说,它会返回未使用的最小值;对于dup2()来说,如果fd2已经被占用,则会先关闭fd2,然后返回fd2;当fd1和fd2相等时,直接返回fd2。


需要注意的是,dup()和dup2()创建的新fd共享传入参数fd的文件标志和文件偏移量

七、fcntl()

fcntl()可以用来获取或设置文件描述符的属性。函数声明如下:

 /* 文件描述符属性控制函数 */
#include <fcntl.h> int fcntl(int fd, int cmd, ...); /* 例子 */
int acc = fcntl(fd, F_GETFL, ); int flg = acc & O_ACCMODE;
if (flg & O_RDONLY)
printf("O_RDONLY\n");
else if (flg & O_WRONLY)
printf("O_WRONLY\n");
else if (flg & O_RDWR)
printf("O_RDWR\n");
else
printf("NONE\n");

函数参数以及返回值:

fd:文件打开函数返回的文件描述符

cmd:命令操作

返回值:成功返回对应的值;出错返回-1。


关于cmd有以下几类:

F_DUPFD:复制一个已有的描述符

F_GETFD、F_SETFD:获取或设置文件描述符标志

F_GETFL、F_SETFL:获取或设置文件状态标志

F_GETLK、F_SETLK、F_SETLKW:获取或设置记录锁,其中F_SETLKW表示若设置记录锁失败,则会等待直至成功

利用fcntl()函数修改文件描述符标志或者文件状态标志时,必须先获取当前的标志状态,然后再追加更新,最后将新的状态标志设置写入回去,如果直接设置会导致旧的标志被复位。

第六节讲解的dup()和dup2()也可以使用fcntl()完成。代码如下:

 /* 相当于dup */
int newfd;
newfd = fcntl(fd, F_DUPFD, );
printf("New Fd: %d\n", newfd); /* 相当于dup2(),区别需要注意:dup()是原子操作 */
close(newfd);
newfd = fcntl(fd, F_DUPFD, newfd);
printf("New Fd: %d\n", newfd);

八、ioctl

ioctl()函数是一个功能比较混杂的函数。通常用于终端I/O,函数声明如下:

 /* 终端I/O控制函数 */
#include <sys/ioctl.h> int ioctl (int fd, unsigned long int request, ...);

九、其他函数

文件创建函数creat(),函数声明如下:

/* 文件创建函数 */
#include <fcntl.h> int creat(const char *pathname, mode_t mode);

此函数有一个致命的缺陷:不是原子操作。因此一般使用open()函数和O_CREAT选项代替。

同步函数sync()、fsync()和fdatasync(),函数声明如下:

 /* 同步函数 */
#include <unistd.h> void sync(void);
int fsync(int fd);
int fdatasync(int fd);

UNIX操作系统中有磁盘缓冲功能,当程序向硬盘写入内容时,并不会每次都去写硬盘,而是将待写入的东西缓存buffer中,在稍后将多次缓存的数据一次性写入硬盘,这种方式称为延迟写。通常内核会在缓冲区满了或者需要重用缓冲区时进行刷新写入。因此提供了上面三个函数,其作用如下:

sync()对整个缓冲区作用生效,不等待实际磁盘操作的结束就返回;

fsync()对指定的文件描述符作用生效,它等待磁盘操作结束才返回;

fdatasync()函数和fsync()函数类似,区别是它只刷新文件的数据部分,不刷新文件的属性部分。

下一章  第四章:文件和目录

第三章:文件I/O的更多相关文章

  1. 第三章 文件 I/O

    3.1 引言 先说明可用的文件 I/O 函数:open.read.write.close,然后说明不同缓冲区长度对read和write函数的影响. 本章所说的函数经常被称为不带缓冲的 I/O (unb ...

  2. apue学习笔记(第三章 文件I/O)

    本章开始讨论UNIX系统,先说明可用的文件I/O函数---打开文件.读写文件等 UNIX系统中的大多数文件I/O只需用到5个函数:open.read.write.lseek以及close open函数 ...

  3. 2 python第三章文件操作

    1.三元运算 三元运算又称三目运算,是对简单的条件语句的简写,如: 简单条件语句: if 条件成立: val = 1 else: val = 2 改成三元运算: val = 1 if 条件成立 els ...

  4. 《UNIX环境高级编程》(APUE) 笔记第三章 - 文件I/O

    3 - 文件I/O Github 地址 1. 文件描述符 对于内核而言,所有打开的文件都通过 文件描述符 (file descriptor) 引用.当打开一个现有文件或创建一个新文件时,内核向进程返回 ...

  5. 第三章 文件IO复习

          open(const char * path, int flag.../*mode_t*/) #include <fcntl.h> path:绝对路径 flag:O_RDONL ...

  6. Windows Pe 第三章 PE头文件(上)

    第三章  PE头文件 本章是全书重点,所以要好好理解,概念比较多,但是非常重要. PE头文件记录了PE文件中所有的数据的组织方式,它类似于一本书的目录,通过目录我们可以快速定位到某个具体的章节:通过P ...

  7. 《Django By Example》第三章 中文 翻译 (个人学习,渣翻)

    书籍出处:https://www.packtpub.com/web-development/django-example 原作者:Antonio Melé (译者注:第三章滚烫出炉,大家请不要吐槽文中 ...

  8. 《Linux内核设计与实现》读书笔记 第三章 进程管理

    第三章进程管理 进程是Unix操作系统抽象概念中最基本的一种.我们拥有操作系统就是为了运行用户程序,因此,进程管理就是所有操作系统的心脏所在. 3.1进程 概念: 进程:处于执行期的程序.但不仅局限于 ...

  9. 精通Web Analytics 2.0 (5) 第三章:点击流分析的奇妙世界:指标

    精通Web Analytics 2.0 : 用户中心科学与在线统计艺术 第三章:点击流分析的奇妙世界:指标 新的Web Analytics 2.0心态:搞定它.新的闪亮系列工具:是的.准备好了吗?当然 ...

随机推荐

  1. Java BigDecimal初探

    更新时间:2016-03-17 一.引言 <Effactive Java>中有这样的描述:float和double类型的主要设计目标是为了科学计算和工程计算.他们执行二进制浮点运算,这是为 ...

  2. [翻译] JTSlideShadowAnimation

    JTSlideShadowAnimation 效果图: JTSlideShadowAnimation allow you to reproduce the famous "slide to ...

  3. CSV输入输出

    读取csv文件: import csv cf = open('D:\pywe.csv','rb') cf.readline() #读取标题行,光标移动到下一行(相当于调过标题行) for l in c ...

  4. TMG 2010 使用脚本来导入URL集和域名集

    作为一个网管,相信有领导叫你限制员工上网的情况,例如只限制员工访问某些网站.在禁止的网站数量少的时候,添加URL集或者域名集是一件很简单的事情,如果禁止的网站数量多达1500个呢?如果再使用ISA S ...

  5. Vue中$refs的用法

    说明:vm.$refs 一个对象,持有已注册过 ref 的所有子组件(或HTML元素) 使用:在 HTML元素 中,添加ref属性,然后在JS中通过vm.$refs.属性来获取 注意:如果获取的是一个 ...

  6. vs中添加工具cmder并自动定位到当前目录

    有时在vs中为了使用git命令行,需要打开cmder工具,并让cmder自切换到当前目录: 方法1: 看下效果: 方法2:在文件夹中右键(添加到右键自行百度)

  7. [零基础学JAVA]Java SE面向对象部分.面向对象基础(03)

     1.静态变量的使用 2.单例模式的使用. 3.构造方法的私有化. 4.string的使用,两种构造的不同. 小的记忆错误: · 数组的长度:数组名称.length     这个没()哈~~ · 字符 ...

  8. svg压缩工具svgo安装使用

    svgo是基于node.js的插件,所以需要先安装node.js 1.安装完node.js后,打开node.js命令窗口,输入npm install -g svgo,安装成后会出现下边的内容 2.对s ...

  9. Ubuntu root 密码忘记-恢复

    @Ubuntu root 密码忘记-恢复 2012-04-27 11:09:22 方法一: 如果用户具有sudo权限,那么直接可以运行如下命令: #sudo su root #passwd #更改密码 ...

  10. extern “C”

    http://www.cnblogs.com/yc_sunniwell/archive/2010/07/14/1777431.html 问题:extern “C” 在C++环境下使用C函数的时候,常常 ...