目录
本节将介绍Linux系统的文件I/O。文件I/O又被称为不带缓冲的I/O,这里的不带缓冲是指每个函数都是一个系统调用,。后面的章节中我们还将介绍另一种带缓冲的I/O,届时我们再对这里的”缓冲”进行说明,并综合比较这两种I/O。
7.1 文件综述
文件(File)是指一个具有符号名字的一组相关联元素的有序序列。Linux系统中有一个经典的思想“一切皆文件”。 和 Windows 系统不同,Linux 系统没有那么多的盘符的概念,只有一个根目录(/),所有的文件(资源)都存储在以根目录(/)为树根的树形目录结构中。
这样做最明显的好处是,开发者仅需要使用一套 API 和开发工具即可调取 Linux 系统中绝大部分的资源。 举个简单的例子,Linux 中几乎所有读(读文件,读系统状态,读 socket,读 PIPE)的操作都可以用 read 函数来进行;几乎所有更改(更改文件,更改系统参数,写 socket,写 PIPE)的操作都可以用 write 函数来进行。
不利之处在于,使用任何硬件设备都必须与根目录下某一目录执行挂载操作,否则无法使用。我们知道,本身 Linux 具有一个以根目录为树根的文件目录结构,每个设备也同样如此,它们是相互独立的。如果我们想通过 Linux 上的根目录找到设备文件的目录结构,就必须将这两个文件系统目录合二为一,这就是挂载的真正含义。
Linux系统中将文件分为以下其中类型:
- 普通文件(-) : ASCII文本文件、二进制文件以及硬链接文件,一般我们可以直接拿来用的都是普通文件,例如pdf文件,txt文件等等;
- 目录文件(d): 目录文件包含了目录中各个文件的文件名以及指向这些文件的指针;
- 符号链接(l): symbol link文件
- 管道文件( p): pipe
- 字符设备 ( c ): 原始的I/O设备文件,每次操作仅操作1个字符(例如键盘)
- 块设备(b): 按块I/O设备文件(例如硬盘)
- 套接字(s) : 用于进程间网络通信,一般隐藏在 /var/run/ 目录下
本小节内容参考如下链接:http://c.biancheng.net/view/2852.html
7.2 文件I/O基本操作
7.2.1文件描述符
每当我们创建或者打开一个文件,内核会向进程返回一个文件描述符。对于内核而言,这些文件都是通过一个文件描述符来引用的,它是一个非负整数,本节介绍的文件I/O相关的函数也都是基于文件描述符来操作的。
Linux系统文件描述符总是从0开始,顺序使用的,而且一般只受到系统配置,存储器大小等限制,除非认为限制,否则基本上是无限的。中shell会把进程的标准输入与文件描述符0关联,标准输出与文件描述符1关联,标准错误与文件描述符2关联。Linux系统中,用STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO来表示这三个文件描述符。
文件描述符可以通过open函数,openat函数,creat函数获取,也可以通过fileno函数。
- 文件路径 到 文件描述符:filepath--open()--fd;
- 文件指针 到 文件描述符:FILE*--fileno()--->fd。
7.2.2 创建、打开、关闭文件
Linux系统中可以用open和openat函数来创建或者打开文件,返回值是当前系统未用的最小文件描述符,先来看下它们的定义。
1. 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
path参数:要创建或者打开的文件名
oflag参数:文件对象的属性,取值如下:
- O_RDONLY: 只读打开;
- O_WRONLY: 只写打开;
- O_RDWR: 读写打开;
- O_EXEC: 执行打开;
以上四个选项必须且只能指定其中一个。后面的这些选项都是可选项,若需使用,与上面的四个属性采用或的方式:
- O_CREAT: 文件不存在,则创建文件,后面的mode参数必须执行,mode参数指定文件访问权限位;
- O_EXCL: 如果同时指定了O_CREAT,则判断文件是否已经存在,存在的话则出错返回;
- O_APPEND:每次写时都追加到文件尾端;
- O_TRUNC: 如果文件已经存在,而且为写或者读写打开,则将文件长度截断为0;
- O_DIRECTORY: 如果文件不为目录则出错;
- O_NOCTTY: 如果path引用的是终端设备,则不将此终端设备作为进程的控制终端;
- O_NOFOLLOW:如果path为符号链接则出错;
- O_NONBLOCK:如果path引用的是FIFO,块特殊文件或字符特殊文件,则将本次的打开及后续I/O操作设置为非阻塞;
- O_SYNC:以同步方式打开,每次write必须等待物理I/O操作完成,包括由write引起的文件属性更新;
- O_DSYNC:每次write必须等待物理I/O操作完成,但如果写操作不影响读取刚写入的数据,则不需要等write引起的文件属性更新;
- O_RSYNC:同O_SYNC。
Open函数创建或打开的文件必须在当前进程的工作目录。而openat函数通过fd参数可以访问到更多的路径:
- 当path参数指定的是绝对路径名,fd参数无效,openat函数同open函数;
- 当path指定的是相对路径名,fd参数为AT_FDCWD,路径名也在当前工作目录获取,openat函数同open函数;
- 当path指定的是相对路径名,fd参数指定了相对路径名在文件系统中的起始地址。
1. Creat函数
#include<fcntl.h>
int creat(const char *path, mode_t mode);
//成功返回文件描述符,失败返回-1
Creat函数以只写的方式创建一个新文件,同:
open(path, O_WRONLY|O_CREAT|O_EXCL,mode);
看这个定义也能直到creat函数是以只写的方式创建一个文件。如果我们需要读这个文件,则需要先close这个文件,再open后才能执行读操作。
2. 关闭文件
#include<unistd.h>
int close(int fd);
//成功返回0,失败返回-1
7.3 读写与定位文件
7.3.1 read函数
#include<unistd.h>
int read(int fd, void *buf, size_t nbytes);
read成功返回读取到的字节数;如果已经读到文件尾端,返回0;出错返回-1。
7.3.2 write函数
#include<unistd.h>
int write(int fd, void *buf, size_t nbytes);
write成功返回已写的字节数,失败返回-1。write函数返回值通常与nbytes相同,否则表示出错。
Write函数同时从当前文件偏移量除开始写。如果打开文件时指定了O_APPEND属性,则当前文件偏移量被设置为文件结尾,写完后,当前文件偏移量更新;如果指定了O_TRUNC属性,则从头开始写,写完后同样更新当前文件偏移量。
7.3.3 lseek
Linux系统中,打开文件时如果没有指定O_APPEND,则文件偏移量默认设置为0,但lseek函数可以为文件设置当前文件偏移量。
#include<unistd.h>
off_t lseek(int fd, off_t offset, int whence);
//成功返回新的文件偏移量,失败返回-1
whence参数指定设置的起始点,而offset参数是相对于起始点的偏移量。
- whence为SEEK_SET, 则将文件的偏移量设置为距文件开始处offset个字节;
- whence为SEEK_CUR,文件的偏移量设置为当前偏移量加offset个字节,offset可正可负;
- whence为SEEK_END,文件的偏移量设置为文件长度加上offset个字节,offset可正可负。
lseek函数是不能用于pipe,fifo,socket的。我们可以用下面的方式来确定当前的文件是否可以设置:
offset curpos;
curpos = lseek(fd,0,SEEK_CUR);
如果curpos为-1,则证明fd文件不能使用lseek函数。
7.3.4 例程
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#define MAXLINE 100
char err[] = "open fail\n";
int main(int argc, char *argv[])
{
int fd;
char buf[MAXLINE];
size_t n;
fd = open("lseek.txt",O_RDWR|O_CREAT|O_EXCL,S_IRWXU);
if(fd == -1)
{
printf("errno: %d\n",errno);
fd = open("lseek.txt",O_RDWR|O_TRUNC);
if(fd == -1)
{
printf("errno: %d\n",errno);
write(STDOUT_FILENO,err,sizeof(err));
return -1;
}
}
do
{
n = read(STDIN_FILENO,buf,MAXLINE);
if(strncmp(buf,"quit",4) == 0)
{
break;
}
if(n < 3)
{
break;
}
if(write(fd,buf,n-1) == -1)
{
return -1;
}
}while(n >= 0);
if(lseek(fd,10,SEEK_END) == -1)
{
printf("lseek fail\n");
return -1;
}
n = read(STDIN_FILENO,buf,MAXLINE);
if(n >= 0)
{
if(write(fd,buf,n-1) == -1)
{
return -1;
}
}
return 0;
}
看下程序的执行结果: 程序中使用lseek函数将文件偏移量设置成当前位置后10个字节。再写入了test字符串。使用od 命令来看下文件的实际内容,可以发现test字符串前有个文件空洞。
文件空洞位于文件中都以’\0’的方式表示,但实际上文件空洞并不占用实际的内存。
7.4 文件共享
Linux系统支持不同的进程共享打开的文件,即一个文件有可能被多个进程同时打开使用,此时我们就需要考虑同步问题,之前介绍的文件锁就可以用于进程间同步使用文件。Glibc中也提供一系列接口来完成共享动作,在此之前我们先来看下linux内核用于IO的数据结构。
上图就是一种典型的文件的数据结构示意图,图中的进程使用两个文件描述符打开了同一个文件。先来看看这三种数据结构:
1. 文件描述符表:每个进程中在PCB中都有一个记录项,记录项中包含一张打开的文件描述符表,表中的每项就是一个描述符,描述符中记录中文件的描述符标志以及它的文件表项地址。
- 文件描述符标志就是close_on_exec,是指exec新的程序时这个文件是否会继承,可以通过后面的fcntl函数来控制;
- 文件表项指针指向该文件的文件表项。
2 . 文件表项:文件表项的信息有:
- 文件状态标志,是指读,写,同步,非阻塞等属性;
- 当前文件偏移量;
- I节点指针,指向实际i节点地址,linux中没有v节点。
3. i节点: 每个文件对应的可能在不同的进程中有文件描述符,也有不同的文件表项,但i节点只有一个。I节点包含了文件的长度,所有者,指向的实际磁盘地址等基本信息。
7.4.1 pread和pwrite
上面有提到过共享文件的时候很容易出现不同步的问题,我们来考虑这种情况:A和B进程打开了同一个文件,然后执行下面的操作:执行A进程时调用lseek将当前文件偏移量设置成1500,准备去写数据;这时CPU被系统切走去执行进程B,进程B也通过lseek将当前文件偏移量设置成1500,并写入了100bytes的数据;再切回A进程去继续写操作A进程的当前文件偏移量还是1500,这样A进程再写入的数据就会将B进程之前写入的覆盖掉。
那如何去避免这种调用lseek后 read或者write的不同步问题呢?Posix.1标准中定义了两个原子操作函数pread和pwrite函数来保证lseek和read/write是一个原子操作,不能被其它程序多打断。
#include<unistd.h>
int pread(int fd, void *buf, size_t nbytes, off_t offset);
int pwrite(int fd, void *buf, size_t nbytes, off_t offset);
//返回值同write和read函数
pread和pwrite相当于调用lseek后调用read和write函数。
7.4.2 dup和dup2
使用dup函数和dup2函数可以复制一个现有的文件描述符,这两个文件描述符共享同一个文件表项。
#include<unistd.h>
int dup(int fd);
int dup(int fd,int newfd);
//成功返回新的文件描述符,失败返回-1
dup函数返回的文件描述符如同open函数,一定是当前系统可用的最小文件描述符。
dup2可以指定新描述符的值,如果fd2已经打开,则先将其关闭,如果fd和 newfd相同,则不会关闭newfd,否则fd2的文件描述符状态标志就会被清掉。
7.4.3 sync函数
文件I/O又被成为不带缓冲的I/O,但这里的不带缓冲是指在用户层不会buffer数据,每个I/O函数都是系统调用,但在内核中是有缓冲的,这种方式被称之为延迟写操作。内核中设有cache,大多数的I/O都通过cache进行,比如我们向文件写入数据,内核通常都是先将数据复制到cache中,然后排入写队列,再一起写入文件。
Glibc中定义了三个函数,可以让cache中的数据立即写入文件。
#include<unistd.h>
int fsync(int fd);
int fdatasync(int fd);
//成功返回新的文件描述符,失败返回-1
void sync(vodi);
这三者的区别是:
- Sync函数只是将修改的缓冲区数据排入写队列,不等实际的写磁盘操作结束就返回;
- Fsync函数只对fd文件起作用,而且需要等实际的写磁盘操作结束,fsync也会同步更新文件属性;
- Fdatasync函数同fsync,唯一个区别是它不会同步更新文件属性。
7.4.4 fcntl函数
Fcntl函数用来获取或者改变文件的属性。
#include<unistd.h>
int fcntl(int fd, int cmd, … /* arg*/);
//成功返回值依赖于cmd,失败返回-1
Linux系统中定义了fcntl函数的五种功能以及相应处理方式。
1. 复制一个已有的描述符
- F_DUPFD: 复制文件描述符,新的描述符是尚未打开的描述符中大于等于第三个参数的最小的那个,新描述符的FD_CLOSEXEC文件描述符标志被清除。
- F_DUPFD_CLOSEXEC: 复制文件描述符, 新描述符的FD_CLOSEXEC文件描述符标志被设置。
前面介绍的dup(fd) = fcntl(fd,F_DUPFD,0); dup2(fd,fd2) = {close(fd2), fcntl(fd,F_DUPFD,fd2) };
2. 获取/设置文件描述符标志
- F_GETFD: 获取文件描述符标志;
- F_SETFD: 根据第三个参数设置文件描述符标志。
当前linux系统中文件描述符标志只有CLOSE_ON_EXEC。
3. 获取/设置文件状态标志
- F_GETFL: 获取文件状态标志;
- F_SETFL: 获取文件状态标志。
文件状态标志是指open 函数中介绍的oflag参数对应的那些选项,比如O_RDONLY。
4. 获取/设置异步I/O所有权
- F_GETOWN:获取当前接收SIGIO和SIGURG信号的进程ID或者进程组ID;
- F_SETOWN:设置当前接收SIGIO和SIGURG信号的进程ID或者进程组ID,正的arg表示进程ID,负的arg表示进程组ID。
5. 文件锁:
关于文件锁,后续讲解高级I/O的时候我们再详细介绍。
7.4.5 Ioctl函数
Ioctl函数是I/O操作的百宝箱,基本上所有的I/O操作都可以有ioctl函数完成。系统为不同种类的设备提供了通常的ioctl命令,每个设备驱动程序也可以定义它自己专用的一组ioctl。后面有机会我们再详细的讲解下这个神通广大的函数。
#include<sys/ioctl.h>
int ioctl(int fd, int request, …);
//成功返回值依赖于request,失败返回-1
7.4.6 /dev/fd
Linux系统还提供了名为/dev/fd的目录项,其目录项名为0,1,2,等的文件。打开文件/dev/fd/n就相当于复制文件描述符n,当然这里的文件描述符n对应的文件必须是打开的。/dev/fd文件主要是给shell使用的,例如某些允许使用路径名作为调用参数的程序,想处理标准输入和输出,我们就可以使用/dev/fd/0作为替代:
filter file2| cat file1 /dev/fd/0 fuke3| lpr
写在文末:本文作为个人对APUE的学习笔记,章节安排和内容基本参考APUE。文中有疏漏或者错误的地方,还请不吝赐教。