前面两章说明了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. 【转载】JSP 获取真实IP地址的代码

    JSP 获取真实IP地址的代码 在JSP里,获取客户端的IP地址的方法是:request.getRemoteAddr(),这种方法在大部分情况下都是有效的.   但是在通过了 Apache,Squid ...

  2. The Dominator of Strings HDU - 6208(ac自动机板题)

    题意: 就是求是否有一个串 是其它所有串的母串 解析: 把所有的串都加入到trie数中  然后用最长的串去匹配就好了 emm..开始理解错题意了...看成了只要存在一个串是另一个的母串就好.. 然后输 ...

  3. 【刷题】BZOJ 3591 最长上升子序列

    Description 给出1~n的一个排列的一个最长上升子序列,求原排列可能的种类数. Input 第一行一个整数n. 第二行一个整数k,表示最长上升子序列的长度. 第三行k个整数,表示这个最长上升 ...

  4. CF1088F Ehab and a weird weight formula 贪心 倍增

    CF1088F Ehab and a weird weight formula 题意 给定一棵树,点有点权,其中这棵树满足除了权值最小的点外,每个点至少有一个点权小于它的相邻点. 要求你重新构建这棵树 ...

  5. Alpha 冲刺 —— 十分之七

    队名 火箭少男100 组长博客 林燊大哥 作业博客 Alpha 冲鸭鸭鸭鸭鸭鸭鸭! 成员冲刺阶段情况 林燊(组长) 过去两天完成了哪些任务 协调各成员之间的工作 学习MSI.CUDA 试运行软件并调试 ...

  6. Java之集合Collection

    集合 初次学习集合过程中的学习笔记,学习顶层,使用底层.包括常用的API Collection接口 概述 集合 : 集合是Java中提供的一种容器,可以用来存储多个数据. 与数组的区别: 数组的长度是 ...

  7. mysql INNER/LEFT/RIGHT JOIN区别

    1.创建table DROP TABLE IF EXISTS `tab_id_index`; CREATE TABLE `tab_id_index` ( `id` ) ', `name` ) DEFA ...

  8. [收藏转载链接]Opencv部分

    转载自-柳如风-http://www.cnblogs.com/rongfangliu/p/opencvlink.html [收藏夹整理]OpenCV部分   OpenCV中文论坛 OpenCV论坛 o ...

  9. logstash定义表达式

    例如: echo '[2018/02/02 08:34:43.032]' >> tomcat_catalina.out 过滤方式 "message" => &qu ...

  10. SQL语句(十八)—— 存储过程

    存储过程 系统存储过程 自定义存储过程 扩展存储过程 一.创建存储过程 创建存储过程 --例1 USE SU GO Create Procedure SelProc AS Select * From ...