UNIX环境高级编程——文件I/O
一、文件描述符
对于Linux而言,所有对设备或文件的操作都是通过文件描述符进行的。当打开或者创建一个文件的时候,内核向进程返回一个文件描述符(非负整数)。后续对文件的操作只需通过该文件描述符,内核记录有关这个打开文件的信息(file结构体)。
一个进程启动时,默认打开了3个文件,标准输入、标准输出、标准错误,对应文件描述符是0(STDIN_FILENO)、1(STDOUT_FILENO)、2(STDERR_FILENO),这些常量定义在unistd.h头文件中。
二、open系统调用
(1)函数原型 int open(const char *path, int flags);
参数
path :文件的名称,可以包含(绝对和相对)路径
flags:文件打开模式
返回值:
打开成功,返回文件描述符;打开失败,返回-1
(2)函数原型 int open(const char *path, int flags,mode_t mode);
参数
path :文件的名称,可以包含(绝对和相对)路径
flags:文件打开模式
mode: 用来规定对该文件的所有者,文件的用户组及系 统中其他用户的访问权限
返回值:
打开成功,返回文件描述符;打开失败,返回-1
打开文件的方式:
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读写打开
O_APPEND 写入的所有数据将被追加到文件的末尾
O_CREAT 打开文件,如果文件不存在则建立文件。需要第三个参数mode,用来指定该文件的访问权限。
O_EXCL 如果已经置O_CREAT且文件存在,则会出错。用此用于检测文件是否存在,如果不存在创建此文件。
O_TRUNC 如果此文件存在,而且为只写或读写成功打开,则将其长度截短为0。
三、creat系统调用
创建一个新文件
函数原型 int creat(const char *pathname, mode_t mode);
注意,此函数等效于: open(pathname,O_WRONLY | O_CREAT | O_TRUNC,mode);
creat的一个不足之处是它以只写方式打开所创建的文件。
四、close()系统调用
函数原型: int close(int fd);
函数参数:
fd :要关闭的文件的文件描述符
返回值:
如果出现错误,返回-1;调用成功返回0
五、read系统调用
一旦有了与一个打开文件描述相关连的文件描述符,只要该文件是用O_RDONLY或O_RDWR标志打开的,就可以用read()系统调用从该文件中读取字节 。
读操作从文件的当前偏移量处开始,在成功返回之前,该偏移量将增加实际读到的字节数。
函数原型: ssize_t read(int fd, void *buf, size_t count);
参数:
fd :想要读的文件的文件描述符
buf : 指向内存块的指针,从文件中读取来的字节放到这个内存块中
count : 从该文件复制到buf中的字节个数
返回值:
如果出现错误,返回-1;读文件结束,返回0;否则返回从该文件复制到规定的缓冲区中的字节数。
六、write系统调用
用write()系统调用将数据写到一个文件中
其返回值通常与参数count的值相同,否则出错。write出错的一个常见原因是:磁盘已写满,或者超过了一个给定进程的文件长度限制。
对于普通文件,写操作从文件的当前偏移量处开始。在一次成功写之后,该文件偏移量增加实际写的字节数。
函数原型: ssize_t write(int fd, const void *buf, size_t count);
函数参数:
fd:要写入的文件的文件描述符
buf: 指向内存块的指针,从这个内存块中读取数据写入 到文件中
count: 要写入文件的字节个数
返回值: 如果出现错误,返回-1;如果写入成功,则返回写入到文件中的字节个数
七、文件的随机读写
到目前为止的所有文件访问都是顺序访问。这是因为所有的读和写都从当前文件的偏移位置开始,然后文件偏移值自动地增加到刚好超出读或写结束时的位置,使它为下一次访问作好准备。
有个文件偏移这样的机制,在Linux系统中,随机访问就变得很简单,你所需做的只是将当前文件移值改变到有关的位置,它将迫使一次read()或write()发生在这一位置。(除非文件被O_APPEND打开,在这种情况下,任何write调用仍将发生在文件结束处)
lseek系统调用:
功能说明:通过指定相对于开始位置、当前位置或末尾位置的字节数来重定位,这取决于 lseek() 函数中指定的位置
函数原型:off_t lseek (int fd, off_t offset, int base);
函数参数:
fd:需要设置的文件描述符
offset:偏移量
base:偏移基位置
返回值:返回新的文件偏移值。文件的当前位置是允许为负数的,所以判断是否成功,不要测试是否小于0,而要测试她是否等于-1.
base 表示搜索的起始位置,有以下几个值:(这些值定义在<unistd.h>)
SEEK_SET 从文件开始处计算偏移
SEEK_CUR 从当前文件的偏移值计算偏移
SEEK_END 从文件的结束处计算偏移
注意:如果文件描述符引用的是一个管道、FIFO或网络套接字,则lseek返回-1,并将errno设置为ESPIPE。lseek只对常规文件有效。
文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0.文件中的空洞并不要求磁盘上占用存储区。
#include "apue.h" #include <fcntl.h> char buf1[] = "abcdefghij"; char buf2[] = "ABCDEFGHIJ"; int main(void) { int fd; if ((fd = creat("file.hole", FILE_MODE)) < 0) err_sys("creat error"); if (write(fd, buf1, 10) != 10) err_sys("buf1 write error"); /* offset now = 10 */ if (lseek(fd, 16384, SEEK_SET) == -1) err_sys("lseek error"); /* offset now = 16384 */ if (write(fd, buf2, 10) != 10) err_sys("buf2 write error"); /* offset now = 16394 */ exit(0); }运行该程序得到:
huangcheng@ubuntu:~$ ./a.out huangcheng@ubuntu:~$ ll file.hole 检查其大小 -rwxr-xr-x 1 huangcheng huangcheng 16394 2013-07-04 14:33 file.hole* huangcheng@ubuntu:~$ od -c file.hole 观察其内容 0000000 a b c d e f g h i j \0 \0 \0 \0 \0 \0 0000020 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 * 0040000 A B C D E F G H I J 0040012
八、sync、fsync和fdatasync函数
传统的unix实现在内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘I/O都通过缓冲进行。当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队列,而是等待其写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时,再将该缓冲排入输出队列,然后待其到达队首时,才进行实际的I/O操作。这种输出方式被称为延迟写。
为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性,UNIX系统提供了sync、fsync和fdatasync三个函数。
int fsync(int fd); int fdatasync(int fd); void sync(void);
sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等于实际写磁盘操作结束。
fsync函数只对由文件描述符fd指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。
fdatasync函数类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。
十、打开文件内核数据结构
1、一个进程打开两个文件
文件状态标志:读、写、追加、同步、非阻塞等
2、一个进程两次打开同一文件
3、两个进程打开同一文件
示例程序:
#include<sys/types.h> #include<sys/stat.h> #include<unistd.h> #include<fcntl.h> #include<stdio.h> #include<stdlib.h> #include<errno.h> #include<string.h> #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0) int main(int argc, char *argv[]) { int fd1, fd2; char buf1[1024] = {0}; char buf2[1024] = {0}; /* 进程控制块PCB * struct task { * ... * struct files_struct *files; * } * 同一个进程两次打开同一个文件,一个进程拥有的一个文件描述符表其中一个fd索引对应的指针指向一个 * 文件表(包括文件状态(读写追加同步非阻塞等),当前文件偏移量, * 文件引用次数(当有两个fd指向同个文件表时引用计数为2,见dup,也可用于重定向), * 文件操作指针, V节点指针等)不共享, * V节点表(包括V节点信息(struct stat), i节点信息等)共享 */ /* 两个进程打开同一个文件的情况与上类同*/ fd1 = open("test.txt", O_RDONLY); if (fd1 == -1) ERR_EXIT("open error"); read(fd1, buf1, 5); printf("buf1=%s\n", buf1); fd2 = open("test.txt", O_RDWR); if (fd2 == -1) ERR_EXIT("open error"); read(fd2, buf2, 5); printf("buf2=%s\n", buf2); write(fd2, "AAAAA", 5); memset(buf1, 0, sizeof(buf1)); read(fd1, buf1, 5); printf("buf1=%s\n", buf1); close(fd1); close(fd2); return 0; }假设test.txt文件的内容是 ABCDEhello
测试如下:
huangcheng@ubuntu:~$ ./a.out buf1=ABCDE buf2=ABCDE buf1=AAAAA huangcheng@ubuntu:~$ cat test.txt ABCDEAAAAA分析:由上图分析可知,一个进程两次打开同一文件,文件表是不共享的,即各有自己的文件偏移量和打开文件标志,所以两次read不同的fd都是从头开始读取,但V节点表是共享的,在fd2写入(同个文件表的read和write是共享偏移的)更改了inode指向的硬盘数据块,再次read fd1得到的也是更改后的值。
十一、I/O重定向
当我们执行了dup(3)之后,系统选择一个空闲的文件描述符即4,这样就有两个文件描述符指向同个文件表,所以引用计数为2。利用dup等函数可以进行重定向的步骤是先close输入输出文件描述符,然后执行dup(fd), 这样输入输出文件描述符也指向fd指向的文件,这样就实现了重定向。此外dup2, fcntl 函数也可以实现,其实不使用这些函数,而直接close(0/1/2)完再open也可以实现。如下使用cat命令实现复制文件的功能:
#include<sys/types.h> #include<sys/stat.h> #include<unistd.h> #include<fcntl.h> #include<stdio.h> #include<stdlib.h> #include<errno.h> #include<string.h> #include<signal.h> #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0) int main(int argc, char *argv[]) { close(0); open("Makefile", O_RDONLY); close(1); open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0664); execlp("cat", "cat", NULL); return 0; }
现在标准输入是文件Makefile,标准输出是文件Makefile2,将当前进程替换成cat,则cat会从标准输入读而后输出到标准输出,即完成了copy的功能。
dup/fcntl 函数示例程序如下:
#include<sys/types.h> #include<sys/stat.h> #include<unistd.h> #include<fcntl.h> #include<stdio.h> #include<stdlib.h> #include<errno.h> #include<string.h> #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0) /* dup dup2 fcntl */ int main(int argc, char *argv[]) { int fd; fd = open("test2.txt", O_WRONLY); if (fd == -1) ERR_EXIT("open error"); /* close(1); dup(fd); */ // dup2(fd, 1); close(1); if (fcntl(fd, F_DUPFD, 0) < 0) //从0开始搜索可用的fd ERR_EXIT("fcntl error"); printf("hello\n"); // 输出重定向到test2.txt return 0; }