3 文件I/O
3.1 引言
3.2 文件描述符
文件描述符是一个标识,非负整数,类似于windows里的句柄,为了与标准C保持一致(标准C里的文件的读写都是通过File Pointer)UNIX采用了这样的三级结构,我混淆于文件描述标志和文件状态标志,还是看英文来的有效,fd 的flag = close_on_exec。是在一个文件在某进程中的标示,而由于文件可以被多个进程打开,因此这个file status flag能被很多个进程访问到,它表示的是这个文件在此刻的读写等标示。
对打开文件的处理与每个描述符的执行时关闭(close_on_exec)标志值有关,进程中每个打开描述符都有一个执行时关闭标志。若设置了此标志,则在执行exec时关闭该描述符;否则该描述符人打开。除非特地用fcntl设置了该执行时关闭标志,否则系统默认是在exec后保持描述符打开状态。
3.3 函数open和openat
<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.
oflag:以怎样的方式来打开或创建文件。
mode:赋予文件读写权限,在第四章中st_mode会有介绍。
O_RDONLY 只读方式打开
O_WRONLY 只写方式打开
O_RDWR 读写打开
O_EXEC 只执行打开
O_SEARCH 只搜索打开(应用于目录)
以上必须且只能指定其中一个,下面的常量是可选的:
O_APPEND 每次写时追加到末尾
O_CLOEXEC 把FD_CLOEXEC常量设置为文件描述符标志
O_CREAT 若此文件不存在,则创建它,使用时,open的第3个选项mode以指定新文件的访问权限位
O_DIRCTORY 如果path引用的不是目录,则出错
O_EXCL 如果同时指定了O_CREAT,而文件已经存在,则出错。可测试一个文件是否存在。
O_NOCTTY 如果path引用的是终端设备,则不将该设备分配作为此进程的控制终端。
O_NOFOLLOW 如果path引用的是一个符号链接,则出错
O_NONBLOCK 如果path引用的是一个FIFO、一个块特殊文件或一个字符特殊文件,则选项为文件的本次打开操作和后续的I/O操作设置非阻塞方式。
O_SYNC 使每次write等待物理I/O操作完成,包括由该write操作引起的文件属性更新所需的I/O.
O_TRUNC 如果此文件存在,而且为只写或读-写成功打开,则将其长度截断为0。
O_TTY_INIT 如果打开一个还未打开的终端设备,设置非标准termios参数值,使符合Single UNIX specification.
下面两个也是可选,是Single UNIX specification中同步输入和输出选项的一部分。
O_DSYNC 使每次write要等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需等待文件属性被更新。
O_RSYNC 使每一个以文件描述符作为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作都完成。
fd参数将两者区分开,当path为相对路径,fd指出开始地址或当前工作目录(AT_FDCWD)
3.4 函数create
#include <fcntl.h>
int creat(const char *path, mode_t mode);
返回值:成功返回只写打开的文件描述符,出错返回-1
下面方式可创建、写、读文件:open(path,O_RDWR|O_CREAT|O_TRUNC,mode);
关闭一个打开的文件
3.5 函数close
#include <unistd.h>
int close(int fd);
返回值:成功返回0;出错返回-1
当一个进程终止时,内核自动关闭它打开的所有文件(隐式关闭)。
3.6 函数lseek
显示地为一个打开文件设置偏移量
<unistd.h>
off_t lseek(int fd, off_t offset, int whence);
返回值:成功返回新的文件偏移量;出错返回-1
whence:
SEEK_SET:将该文件的偏移量设置为距文件开始处offset个字节。
SEEK_CUR:将该文件的偏移量设置为当前值加offset,offset可正可负。
SEEK_END:将该文件的偏移量设置为文件长度加offset,可正可负。
如果文件描述符指向的是一个管道、FIFO或网络套接字,则lseek返回-1,并将errno设置为ESPIPE。
3.7/8 函数read和write
<unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
返回值:读到的字节数,若到文件结尾,返回0;出错返回-1。
ssize_t write(int fd, const void *buf, size_t nbytes);
返回值:成功返回已写的字符数;出错返回-1
3.9 I/O的效率
Richard Steven通过程序实验发现系统CPU时间的最小值在缓冲空间大小为4096个字节以后,继续增加缓冲区长度对读操作所用时间几乎没有影响。
3.10 文件共享
UNIX系统支持不同进程间共享打开文件。首先介绍内核用于所有I/O的数据结构:
内核使用3种数据结构表示打开文件,他们之间的关系决定了文件共享方面一个进程对另一个进程可能产生的影响:
(1)每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项,与灭一个文件描述符相关联的是:
a,文件描述标志(close_on_exec,参见图3-7和3-14)
b,指向一个文件表项的指针。
(2)内核为所有打开文件维护昂文件表,每一文件表项包含:
1) 文件状态标志(读、写、添写、同步和非阻塞等)
2) 当前文件偏移量
3) 指向该文件v节点表项的指针。
(3)每个打开文件(或设备)都有一个v节点(v-node)结构。v节点包含了文件类型和对此文件进行各种操作函数的指针。还包含了该文件的i节点(i-node,索引节点),这些信息实在打开文件时从磁盘上读入内存的,所以,文件的所有相关信息都是随时可用的。例如,i节点包含文件所有者、文件长度、只想文件实际数据块 在磁盘上所在位置的指针等。
这里书上有几张帮助理解进程表项、文件表项及v节点表项的图,笔记格式原因就没有放,总的图我会做在后面的笔记中。
3.11 原子操作
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
返回值:读到的字节数,若已到文件尾,返回0;若出错,返回-1
ssize_t pwrite(int fd, const void *buf,size_t nbytes, off_t offset);
返回值:成功,返回已写的字节数;若出错,返回-1
调用pread相当于调用lseek后再调用read,具体代码如下:
if (lseek(fd, OL, 2) < 0) /* position to EOF */
err_sys("lseek error");
if (read(fd, buf, 100) != 100)
err_sys("read error");
单进程上面没有问题,但是多进程若同时使用上方法,就会产生偏移量错乱的问题,所以pread与上方法调用有重要区别:
1,调用pread是,无法中断其定位和读操作;
2,不更新当前文件偏移量。
这样保证每个进程运行时是安全的,pwrite函数也是类似。
3.12 函数dup和dup2
复制一个现有的文件描述符
#include <unistd.h>
int dup(int fd);
int dup2(int fd,int fd2);
返回值:成功返回新的文件描述符;出错返回-1
3.13 函数sync、fsync和fdatasync
延迟写:内核通常先将数据复制到缓冲区,然后排入队列,晚些时候在写入磁盘。
为了保证磁盘上实际文件系统与缓冲区中的内容的一致性,UNIX系统提供了sync、fsync和fdatasync三个函数
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
返回值:成功返回0;出错返回-1
void sync(void);
sync只是将所有修改过得块缓冲区排入写队列,然后就返回,他并不等实际写磁盘操作结束。通常称为Update的系统守护进程周期性的调用(一般30s)sync函数,这就保证定期冲洗(flush)内核的块缓冲区。
fsync函数只对文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。
fdatasync函数类似于fsync,但他只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。
3.14 函数fcntl
可以改变已经打开文件的属性。
#include <fcntl.h>
int fcntl(int fd, int cmd,... /* int arg */);
返回值:成功,则依赖于cmd;出错返回-1
第三个参数则是指向一个结构的指针。
5种功能:
1,复制一个已有的描述符:(cmd=F_DUPFD或F_DUPFD_CLOEXEC)
2,获取/设置文件描述符标志:(cmd=F_GETFD或F_SETFD)
3,获取/设置文件状态的标志:(cmd=F_GETFL或F_SETFL)
4,获取/设置异步I/O所有权: (cmd=F_GETOWN或F_SET).
5,获取/设置记录锁: (cmd=F_GETLK、F_SETLK或F_SETLKW)
cmd的前8种,与进程表项中各文件描述符相关联的 文件描述符标志 以及每个文件表项中的 文件状态标志。
F_DUPFD 复制文件描述符fd。新文件描述符作为函数值返回。
F_DUPFD_CLOEXEC 复制文件描述符,设置与新描述符关联的F_CLOEXEC文件描述符标志的值,返回新文件描述。
F_GETFD 对应于fd的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志F_CLOEXEC
F_SETFD 对于fd设置文件描述符标志。新标志按第3个参数设置。
F_GETFL 对应于fd的文件状态标志作为函数值返回。
F_SETFL 将文件状态标志设置为第3个参数的值。
F_GETOWN 获取但前接收SIGIO和SIGURG信号的进程ID或进程组ID。
F_SET 设置接收SIGIO和SIGURG信号的进程ID或进程组ID,+arg指定一个进程ID,-arg表示等于arg绝对值的一个进程组ID。
F_GETLK 判断由flockptr所描述的锁是否会被另外一把锁所排斥(阻塞),如果存在一把锁,他阻止创建由flockptr所描述的锁,则把该现存的锁的信息写到flockptr指向的结构中。如果不存在,则除了将l_type设置为F_UNLCK之外,flockptr所指向结构中的其他信息保持不变。
F_SETLK 设置由flockptr所描述的锁。如果试图建立一把读锁(l_type设为F_RDLCK)或写锁,而按上述兼容性规则不能允许,则fcntl立即出错返回,此时errno设置为EACCES或EAGAIN。
F_SETLKW 这是F_SETLK的阻塞版本(W表示wait)。如果因为当前在所请求区间的某个部分另一个进程已经有一把锁,应而按兼容性规则由flockptr所请求的锁不能被创建,则是调用进程休眠。如果请求创建的锁已经可用,或者休眠由信号中断,则该进程被唤醒。
在open函数中说明了文件状态标志:
文件状态标志 | 说明 |
---|---|
O_RDONLY | 只读方式打开 |
O_WRONLY | 只写方式打开 |
O_RDWR | 读写打开 |
O_EXEC | 只执行打开 |
O_SEARCH | 只搜索打开(应用于目录) |
O_APPEND | 每次写时追加到末尾 |
O_NONBLOCK | 非阻塞模式 |
O_DSYNC | 等待写完成(仅数据) |
O_RSYNC | 同步读和写 |
O_SYNC | 等待写完成(数据和属性) |
O_ASYNC | 异步I/O(仅FreeBSD和Mac OS X) |
遗憾的是,5个访问方式标志(O_RDONLY、O_WRONLY、O_RDWR、O_EXEC、O_SEARCH)并不各占1位(由于历史原因,前3个标志的值分别是0,1,2.这5个值互斥,一个文件访问方式只能取其一)。因此首先必须用屏蔽字O_ACCMODE取得访问方式,然后将结果与这5个值分别比较。
int fcntl(int fd, int cmd,… /* struct flock flockptr /);
第三个参数flockptr指向flock结构的指针:
struct flock{
short l_type; /* F_RDLCK, F_WRLCK, or F_UNLCK */
off_t l_start; /* offset in bytes, relative to l_whence */
short l_whence; /* SEEK_SET, SEEK_CUR, or SEEEK_END */
off_t l_len; /* length, in bytes; 0 means lock to EOF */
pid_t l_pid; /* returned with F_GETLK */
};
3.15 函数ioctl
ioctl函数是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。
#include <unistd.h>
#include <sys/ioctl.h>
int ioctl(int fd, ind cmd, …);
return: other value, error:-1.
ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数来控制设备的I/O通道。
ioctl的必要性:
如果不用ioctl的话,也可以实现对设备I/O通道的控制,但那是蛮拧了。例如,我们可以在驱动程序中实现write的时候检查一下是否有特殊约定的数据流通过,
如果有的话,那么后面就跟着控制命令(一般在socket编程中常常这样做)。但是如果这样做的话,会导致代码分工不明,程序结构混乱,程序员自己也会头昏眼花的。
所以,我们就使用ioctl来实现控制的功能。要记住,用户程序所作的只是通过命令码(cmd)告诉驱动程序它想做什么,至于怎么解释这些命令和怎么实现这些命令,
这都是驱动程序要做的事情。
“幻数”是一个字母,数据长度也是8,用一个特定的字母来标明设备类型,这和用一个数字是一样的,只是更加利于记忆和理解。
cmd参数如何得出 这里确实要说一说,cmd参数在用户程序端由一些宏根据设备类型、序列号、传送方向、数据尺寸等生成,这个整数通过系统调用传递到内核中的驱动程序,再由驱动程序使用解码宏从这个整数中得到设备的类型、序列号、传送方向、数据尺寸等信息,然后通过switch{case}结构进行相应的操作。
小结 :
ioctl其实没有什么很难的东西需要理解,关键是理解cmd命令码是怎么在用户程序里生成并在驱动程序里解析的,程序员最主要的工作量在switch{case}结构中,因为对设备的I/O控制都是通过这一部分的代码实现的。
3.16 /dev/fd
某些系统提供路径名/dev/stdin
、/dev/stdout
和/dev/stderr
,这些等效于/dev/fd/0
、/dev/fd/1
和/dev/fd2
.
/dev/fd文件主要由shell使用,它允许使用路径名作为调用参数的程序,能用处理其他路径名的相同的方式处理标准输入输出。