前面两章说明了UNIX系统体系和标准及其实现,本章具体讨论UNIX系统I/O实现,包括打开文件、读文件、写文件等。

UNIX系统中的大多数文件I/O只需要用到5个函数:open、read、write、lseek、close。它们是不带缓冲的I/O。

只要涉及多个进程间共享资源,原子操作的概念就变得很重要,本章通过open( )函数来讨论此概念。

文件描述符

文件描述符是一个非负整数,它是内核对打开文件的一个抽象。每当打开或者创建一个文件时,内核会向进程返回一个文件描述符,随后可以利用该描述符来进行文件的读或写。一个进程默认的文件描述符范围是有限的,可以通过调用sysconf( _SC_OPEN_MAX )函数来查看限制,也可以通过shell命令ulimit -n来查看。例如,在我的Ubuntu Server上,其限制为65536,如下图所示:

而在我的Mac OS X上,则默认最大为256:

函数open和openat

函数open和openat用于打开或创建一个文件。其头文件及函数原型如下:

#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 */ );

这两个函数成功时,返回非负的文件描述符,出错时返回-1。由open和openat函数返回的文件描述符一定是最小的未用的描述符数值。

函数create

函数create用于创建文件。其头文件及函数原型如下:

#include <fcntl.h>

int creat (const char* file, mode_t  mode);

此函数存在致命缺点,即创建和写不是原子操作,因此已经成为一个鸡肋接口。

函数close

函数close用于关闭一个已经打开的文件。其头文件及函数原型如下:

#include <unistd.h>

int close (int fd);

关闭一个文件时会释放该进程加在其上的文件记录锁。当一个进程终止时,内核为自动关闭该进程打开的所有文件。

函数lseek

每个文件都有一个与其相关联的“当前文件偏移量”,它通常是一个非负整数,用来度量从文件开始处计算的字节数。读写操作通常从当前文件偏移量处开始,并使偏移量增加读写的字节数。打开一个文件时默认文件偏移量为0,若指定了O_APPEND选项,则偏移量设置为末尾字节。我们可以使用lseek来手动设置文件偏移量。其头文件及函数原型如下:

#include <unistd.h>

off_t lseek (int fd, off_t offset, int whence)

其中的whence指的是偏移量设置方式,其值有如下三种:

  • SEEK_SET:将文件偏移量从开始处开始偏移,offset只能正值
  • SEEK_CUR:将文件偏移量从当前处开始偏移,offset可正可负
  • SEEK_END:将文件偏移量从文件尾开始偏移,offset可正可负

如果lseek执行成功,则返回新的文件偏移量。lseek也可以用来测试目标文件是否支持设置偏移量。

对于SEEK_CUR和SEEK_END,当文件偏移量设置为负数并且lseek成功执行,则返回的文件偏移量是实际偏移量,而不是设置的offset值,例如:

#include <unistd.h>
#include <fcntl.h>
#include <iostream> using std::cout;
using std::endl; int main()
{
auto fd = open("/file",O_RDONLY);
cout << fd << endl;
cout << lseek(fd,-,SEEK_END);
close(fd);
return ;
}

假定/file是一个文本文件,其内容为“abcde”,则当上面代码中leesk执行成功后,lseek返回值为4,而不是-2,因为我们指定从文件末尾处(SEEK_END)开始进行偏移,偏移量向前(-2),则实际偏移量移动到“d”,被移动经过的第二个是“e”,而第一个是Linux系统上文本末尾的结束标记字符“$”。

文件偏移量的设置可以大于文件的长度,在这种情况下,下一次对文件的读写会加长文件,并在文件中间构成一个空洞,空洞部分被读取为0,空洞部分并不占用硬盘空间。  

函数read

函数read用于从打开的文件读取数据。其头文件及函数原型如下:

#include <unistd.h>

ssize_t read (int fd, void *buf, size_t nbytes)

ssize_t在Linux系统上是一个long int类型。fd是待读取的源文件,buf是待写入的目标缓冲,而nbytes则是想要读取的最大字节数。read函数成功之后返回读取的实际字节数。

  • 返回的字节数和想要读取的最大字节数可能不一致,原因有如下几个:
  • 即将到达文件尾部,而剩余的字节数小于要读取的字节数;
  • 从终端设备读取时,是以换行为准,指定的字节数大于一行的总字节数时;
  • 从网络读时,缓冲导致小于想要读取的字节数;
  • 从面向记录的设备读时,一次最多返回一个记录;
  • 信号中断导致只读取部分的返回。

函数write

函数write用于向打开的文件写入数据。其头文件及函数原型如下:

#include <unistd.h>

ssize_t write (int fd, const void* buf, size_t n);

write函数返回值通常等于n,也即指定写入的数量,否则返回-1表示出错。

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

I/O的效率

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

原子操作

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

函数dup和dup2

函数dup和dup2用来复制一个现有的文件描述符。其头文件及函数原型如下:

#include <unistd.h>

int dup (int fd);
int dup2(int fd1, int fd2);

这两个在成功执行时返回新的描述符,当失败时,它们返回-1。对于dup2( )来说,如果fd2已经被占用,其会先关闭旧的fd2,然后返回与fd2相等的描述符值,当fd1和fd2相等时,其什么也不做,仅仅返回fd2。

函数sync、fsync、fdatasync

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

#include <unistd.h>

void sync(void);
int fsync(int fd);
int fdatasync(int fd); 

其中,fdatasync( )函数在FreeBSD及其衍生版(比如MacOS)中不受支持。

sync( )函数是对整个缓冲区作用生效,并且不等待实际磁盘操作的结束就返回;fsync( )函数是只对指定的文件描述符作用生效,它等待磁盘操作结束才返回。fdatasync( )函数和fsync( )函数类似,区别是它只刷新文件的数据部分,不刷新文件的属性部分。

函数fcntl

函数fcntl( )可以用来设置文件描述符的属性。其头文件及函数原型如下:

#include <fcntl.h>

int fcntl (int fd, int cmd, ...);

fcntl( )函数成功时返回对应的值,失败时返回-1。它具有以下5种功能:

  • 1.复制一个已有的描述符;
  • 2.获取或设置文件描述符标志;
  • 3.获取或设置文件状态标志;
  • 4.获取或设置异步I/O所有权;
  • 5.获取或设置记录锁。

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

函数ioctl

ioctl( )函数是一个功能比较混杂的函数。通常用于终端I/O,其头文件及函数原型如下:

#include <sys/ioctl.h>

int ioctl (int fd, unsigned long int request, ...);

习题

3.1 当读/写磁盘文件时,本章中描述的函数确实是不带缓冲机制的吗?请说明原因。

最终的硬盘I/O是带缓冲的,因为内核会提供一个缓冲区用来存储向硬件设备中写入的数据。对于普通概念上的缓冲,通常是指非内核提供的用户级缓冲。

3.2 编写一个与3.12节中dup2功能相同的函数,要求不调用fcntl函数,并且要有正确的出错处理。

int dup2_self(int fd, int fd2)
{
if (fd < || fd2 < || fd2 > OPEN_MAX) //判断文件描述符的合法性
{
return -;
}
if (fd == fd2)
{
return fd2;
} close(fd2); //如果已打开fd2,则关闭。未打开也不会有影响。 if (fd2 == )
{
return dup(fd); //dup()总是返回最小的,如果是0,则close()关闭后,一定返回0
} int *fdp = new int[(sizeof(int) * fd2)]{}; //全部默认初始化为0,执行到此处时fd2一定大于0
int tempfd = -1, i = ;
while ((tempfd = dup(fd)) != fd2 && tempfd != -1)
{
fdp[i] = tempfd;
++i;
}
while (i+)
{
close(fdp[i--]);
}
delete[] fdp;
return tempfd;
}

3.3 假设一个进程执行下面3个函数调用:

fd1 = open(pathname, oflags);
fd2 = dup(fd1);
fd3 = open(pathname, oflags);

画出类似于图3-9的结果图。对fcntl作用于fd1来说,F_SETFD命令会影响哪一个文件描述符?F_SETFL呢?

3.4 在许多程序中都包含下面一段代码?

dup2(fd, );
dup2(fd, );
dup2(fd, );
if (fd > )
close(fd);

为了说明if语句的必要性,假设fd是1, 画出每次调用dup2时3个描述符项及相应的文件表项的变化情况。然后再画出fd为3的情况。

3.5 在Bourne shell、Bourne-again shell和Korn shell中,digit1>&digit2表示要将描述符digit1重定向至描述符digit2的同一文件。请说明下面两条命令的区别。

./a.out > outfile 2>&1

./a.out 2>&1 > outfile

(提示:shell从左到右处理命令行。)

3.6 如果使用添加标志打开一个文件以便读、写,能否使用lseek在任一位置开始读?能否用lseek更新文件中任一部分的数据?请编写一段程序以验证之。

UNIX环境高级编程 第3章 文件I/O的更多相关文章

  1. UNIX环境高级编程 第4章 文件和目录

    第三章说明了关于文件I/O的基本函数,主要是针对普通regular类型文件.本章描述文件的属性,除了regular文件还有其他类型的文件. 函数stat.fstat.fstatat和lstat sta ...

  2. UNIX环境高级编程 第5章 标准I/O库

    本章是关于C语言标准I/O库的,之所以在UNIX类系统的编程中会介绍C语言标准库,主要是因为UNIX和C之间具有密不可分的关系.由于UNIX系统存在很多实现,而每个实现都有自己的标准I/O库,为了统一 ...

  3. UNIX环境高级编程 第1章 UNIX基础知识

    所有操作系统都为运行在它之上的程序提供各种服务,典型的服务包括:执行新程序.打开文件.读写文件.分配存储空间.提供时间等. UNIX体系结构 严格来说,操作系统是一种软件,它控制计算机硬件资源,提供程 ...

  4. UNIX系统高级编程——第四章-文件和目录-总结

    文件系统: 以UNIX系统V文件系统为例: 磁盘分为区,每个分区都有自己的文件系统: ​ i节点是固定长度的记录项,包含了文件的相关信息.目录项包含文件名和i节点号.stat结构中除文件名和i节点编号 ...

  5. UNIX环境高级编程 第6章 系统数据文件和信息

    UNIX系统的正常运作需要用到大量与系统有关的数据文件,例如系统用户账号.用户密码.用户组等文件.出于历史原因,这些数据文件都是ASCII文本文件,并且使用标准I/O库函数来读取. 口令文件 /etc ...

  6. UNIX环境高级编程 第8章 进程控制

    本章是UNIX系统中进程控制原语,包括进程创建.执行新程序.进程终止,另外还会对进程的属性加以说明,包括进程ID.实际/有效用户ID. 进程标识 每个进程某一时刻在系统中都是独一无二的,它们之间是用一 ...

  7. UNIX环境高级编程 第7章 进程环境

    本章涉及C/C++程序中main函数是如何被调用的.命令行参数如何传递给main函数.程序的内存空间布局.程序如何使用环境变量.程序如何终止退出. main函数 C程序或C++程序总是从main函数开 ...

  8. UNIX环境高级编程 第13章 守护进程

    守护进程daemon是一种生存周期很长的进程.它们通常在系统引导时启动,在系统关闭时终止.守护进程是没有终端的,它们一直在后台运行. 守护进程的特征 在Linux系统中,可以通过命令 ps -efj ...

  9. UNIX环境高级编程 第9章 进程关系

    在第8章学习了进程的控制原语,通过各种进程原语可以对进程进行控制,包括新建进程.执行新程序.终止进程等.在使用fork( )产生新进程后,就出现了进程父子进程的概念,这是进程间的关系.本章更加详细地说 ...

随机推荐

  1. 2013南京网赛1003 hdu 4750 Count The Pairs

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4750 题意:给出一个无向图,f(a,b)表示从点a到点b的所有路径中的每条路径的最长边中的最小值,给出 ...

  2. Python多线程获取返回值

    在使用多线程的时候难免想要获取其操作完的返回值进行其他操作,下面的方法以作参考: 一,首先重写threading类,使其满足调用特定的方法获取其返回值 import threading class M ...

  3. background的使用

    先简单的介绍颜色表示法 一共有三种:单词,rgb表示法,十六进制表示法 rgb:红色 绿色 蓝色 三原色 光学显示器,每个像素都是有三原色的发光原件组成的,靠明亮度不同调成不同的颜色的 有逗号隔开,r ...

  4. Ubuntu和Windows相互共享文件夹

    一.Ubuntu访问Windows共享文件夹 1.对需要共享文件夹右击->属性->共享 2.选择要与其共享的用户,选择好用户点击有点添加按钮添加,然后点击下方的共享按钮 3.完成共享 4. ...

  5. Qt——线程类QThread

    本文主要介绍Qt中线程类QThread的用法,参考(翻译+修改)了一篇文章:PyQt: Threading Basics Tutorial,虽然使用的是PyQt,但与C++中Qt的用法大同小异,不必太 ...

  6. Intelligent Factorial Factorization LightOJ - 1035(水题)

    就是暴力嘛...很水的一个题... 不好意思交都... #include <iostream> #include <cstdio> #include <sstream&g ...

  7. oracle +plsql装完省略号不能点

    1.如图 2.复制 TNS 服务名 3.复制到 登录框的 Database ,输入用户名密码,点OK..可以进去了,省略号变成可点击状态

  8. 如何让自己的广播只让指定的 app 接收?

    1.自己的应用(假设名称为应用 A)在发送广播的时候给自己发送的广播添加自定义权限,假设权限名为:com.itheima.android.permission , 然后需要在应用 A 的 Androi ...

  9. php高效遍历文件夹、高效读取文件

    /** * PHP高效遍历文件夹(大量文件不会卡死) * @param string $path 目录路径 * @param integer $level 目录深度 */ function fn_sc ...

  10. (转)编码规范系列(一):Eclipse Code Templates设置

    背景:长久以来,对java编程中的注释不甚理解.再次学习<疯狂JAVA讲义>基础,深深的感到自己基本功的不牢固.所以要做到事无巨细,好好修炼. 认识注释 常识 注释的作用: 回顾原有的代码 ...