今天看得挺快的,一下子就把第二章看完了,不过第二章也确实看得不仔细,这一章其实在程序设计中还是非常重要的,因为这一章的内容决定了程序的可移植性。
好了,回到这一章的主题文件I/O。
3.2节主要对文件描述符的概念进行了简单的介绍。根据APUE:文件描述符是一个非负整数。当进程打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。我也简单地翻了一下LKD和《深入理解linux内核》,其中对于文件描述符的讲解不是很多,所以对于文件描述符也谈不出来太深入理解,给大家还是分享一篇blog吧。
http://blog.csdn.net/cywosp/article/details/38965239
linux系统也将文件描述符0、1、2,分别与进程的标准输入、标准输出、标准错误相关联。在编程过程中,可使用如下定义:
#define STDIN_FILENO 0 /* Standard input. */ #define STDOUT_FILENO 1 /* Standard output. */ #define STDERR_FILENO 2 /* Standard error output. */
文件描述符的变化范围是0-OPEN_MAX-1。使用“ulimit -n”命令可以查询当前shell以及由它启动的进程可拥有的文件描述符数量。在我的机器上结果是1024,通过“ulimit -n n”命令就可以修改,其中最后一个n表示最大的文件描述符数量。但上述方法只能在当前终端有效,退出之后又恢复为默认值。还可以通过修改/etc/下的文件的方式进行修改,但我对这几个文件的关系还不太清晰,在此就不给大家分享了。以上是修改某个shell的最大文件描述符数量的方法,再来看看修改系统级数值的方法,修改之前先要查看一下相关内容,可通过如下命令进行查询“sudo cat /proc/sys/fs/file-max”,在我的机器上结果是“402307”。修改的方法是通过“6553560 > /proc/sys/fs/file-max”或“sysctl -w "fs.file-max=34166" ”命令,但以上命令在机器重启后失效,所以修改/etc文件的方法才是一劳永逸的方法。
上述修改内容学习自这篇blog:“http://coolnull.com/2796.html”。
以上都是有关于OPEN_MAX的内容,在<stdio.h>中还定义有“# define FOPEN_MAX 16”(确切的说这一定义位于/usr/include/x86_64-linux-gnu/bits/stdio-lim.h中)。
3.3节正式开始有关于编程的内容,首先是打开或创建一个文件,在我的机器中有如下定义:
#include <fcntl.h> #ifndef __USE_FILE_OFFSET64 extern int open (const char *__file, int __oflag, ...) __nonnull ((1)); #else # ifdef __REDIRECT extern int __REDIRECT (open, (const char *__file, int __oflag, ...), open64) __nonnull ((1)); # else # define open open64 # endif #endif #ifdef __USE_LARGEFILE64 extern int open64 (const char *__file, int __oflag, ...) __nonnull ((1)); #endif # ifndef __USE_FILE_OFFSET64 extern int openat (int __fd, const char *__file, int __oflag, ...) __nonnull ((2)); # else # ifdef __REDIRECT extern int __REDIRECT (openat, (int __fd, const char *__file, int __oflag, ...), openat64) __nonnull ((2)); # else # define openat openat64 # endif # endif # ifdef __USE_LARGEFILE64 extern int openat64 (int __fd, const char *__file, int __oflag, ...) __nonnull ((2)); # endif #endif因为是64位的机器,所以有一些64位的函数,open64、openat64。关于文件名实在没什么可说的,关于oflag选项,给大家分享一点,这些常量定义位于/usr/include/fcntl.h(根据书中所写),实际上在该文件中还有一句“#include <bits/fcntl.h>”,这个文件中其实还不包括我们所要找的东西,/usr/include/x86_64-linux-gnu/bits/fcntl-linux.h文件中才是我们所要找的文件,具体内容就不给大家分享了,其中包括有“O_RDONLY”、“O_WRONLY”、“O_RDWR”三个选项,书中还给出了“O_EXEC”与“O_SEARCH”这两个选项,但在我的文件中并没有找到,所以结合书中的内容——“O_RDONLY”、“O_WRONLY”、“O_RDWR”这三个标志必须指定一个且只能指定一个。标志之间使用“|”运算。“...”参数代表文件访问权限的初始值,这一参数仅在 第二个参数中有O_CREAT时才用作用。若没有,则第三个参数可以忽略。来看几个例子:
首先是打开不存在的文件,源码如下:
#include <stdio.h> #include <fcntl.h> #include <errno.h> int main(int argc,char *argv[]) { int n; if((n = open("./temp",O_RDWR))<0) perror(argv[0]); return 0; }
gcc -o test_opennotcreate test_opennotcreate.c /test_opennotcreate ./test_opennotcreate: No such file or directory
#include <stdio.h> #include <fcntl.h> #include <errno.h> int main(int argc,char *argv[]) { int n; if((n = open("./temp",O_RDWR|O_CREAT,S_IRUSR))<0) perror(argv[0]); if((n = write(fd,str,strlen(str)))<0) perror(argv[0]); return 0; }执行结果如下:
gcc -o test_opencreate test_opencreate.c ./test_opencreate 此时内容成功写入 ./test_opencreate 再次执行程序 ./test_opencreate: Permission denied ./test_opencreate: Bad file descriptor
出现了无权限创建文件的情况,不知道是由于权限设置的问题,还是由于O_CREATE标志不能用于已存在的文件。今天早上我又研究了一下,errno中有一个EEXIST的错误,代表文件已存在,但我的程序给出的错误码是-1(“无权限”),所以我觉得我应该是权限这一块还有问题没搞清楚,加上一个”用户写权限“试试:
int main(int argc,char *argv[]) { int fd,n; char str[] = "Hello,world"; if((fd = open("./temp",O_RDWR|O_CREAT,S_IRUSR|S_IWUSR))<0){ perror(argv[0]); } if((n = write(fd,str,strlen(str)))<0) perror(argv[0]); return 0; }
./test_opencreate ./test_opencreate 再次运行程序也可以正常运行
看来是由于之前没有写权限导致不能重复打开已存在的文件(此处还是要存下一个疑问,为什么加上一个写权限后就能重复打开打开文件),同时通过实验结果可以发现,每次创建或打开已经存在的文件,都会从文件起始处开始写入,另一方面上述程序也证明了O_CREAT同样可以用于打开已经存在文件,那么问题来了如果两次打开文件的权限不一样怎么办?
再来加上一个O_APPEND选项再试试:
if((fd = open("./temp",O_RDWR|O_CREAT|O_APPEND,S_IRUSR|S_IWUSR))<0){ perror(argv[0]); }
if((fd = open("./temp",O_RDWR|O_CREAT|O_APPEND|O_TRUNC,S_IRUSR|S_IWUSR))<0){ perror(argv[0]); }
这里给大家谈一点我对O_CREAT与O_TRUNC区别的理解,若只有O_CREAT选项,那么文件的写入总是从文件起始处写入,若文件已经存在,那么会覆盖原有文件开头部分的内容,后面的内容可能被保留。若使用O_CREAT|O_TRUNC选项,若文件已经存在而且为只写或读写成功打开,那么首先将其长度截断为0,即原文件中的内容被全部删除。
根据APUE,使用fd参数把open和openat函数区分开,共有3三种可能性。
突然一看openat函数有些鸡肋,只是为了在某个文件夹下创建文件就要补充一个新的函数,具体openat函数有什么作用我也不是很清楚。APUE怎么说,我就先给大家直接分享过来。
3.4 creat函数原型如下:
#include <fcntl.h> int creat(const char *path,mode_t mode);
open(path,O_WRONLY|O_CREAT|O_TRUNC,mode);
3.6 lseek函数,I/O函数的读写操作通常都从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认的情况,当打开一个文件时,除非指定O_APPEND,否则该偏移量被设置为0,可以调用lseek显示地为一个打开文件设置偏移量,函数原型如下:
#include <unistd.h> off_t lseek(int fd,off_t offset,int whence)若成功返回新的偏移量;若出错则返回-1。
whence参数共包括三种选择,分别是:
SEEK_SET:将该文件的偏移量设置为距文件开始处offset个字节。
SEEK_CUR:将该文件的偏移量设置为其当前值加offset,offset可正可负。
SEEK_END:将该文件的偏移量设置为文件长度加offset,offset可正可负。
APUE中给出了一种确定当前文件偏移量的方法:
off_t currpos; currpos = lseek(fd,0,SEEK_CUR);
上述方法也可用于确定所涉及的文件是否可以设置偏移量。若文件描述符指向管道、FIFO(命名管道)、网络套接字,则lseek返回-1,并将errno设置为ESPIPE。
APUE中给出了用于测试标准输入能否设置偏移量的例子,源码如下:
#include <stdio.h> #include <unistd.h> #include <errno.h> int main(int argc,char* argv[]) { if( lseek(STDIN_FILENO,0,SEEK_CUR) == -1) perror(argv[0]); else printf("seek ok\n"); return 0; }
./test_lseek ./test_lseek: Illegal seek
#define ESPIPE 29 /* Illegal seek */
结合之前的描述可以得到结论:标准输入是管道或FIFO。
通常,文件的当前偏移量应当是一个非负整数,但是,某些设备也可能允许负的当前偏移量。对于普通文件,其偏移量必须是非负值(关于文件的类型之后会详细分析)。由于偏移量可能是负值,所以在比较lseek的返回值应当谨慎,不要测试它是否小于0,而要测试它是否等于-1。lseek仅将当前的文件偏移量记录在内核中,这个偏移量可用于下一个读写操作。文件的偏移量可以大于当前文件的长度,在这种情况下,对该文件的下一次写将加长该文件,并将这部分文件的内容填充为0,这部分没有内容的文件被称为“文件空洞”。“文件空洞”并不需要占据磁盘空间。
通过实验验证一下:
#include <fcntl.h> #include <stdio.h> char buf1[] = "abcdefghij"; char buf2[] = "ABCDEFGHIJ"; int main(int argc,char* argv[]) { int fd; if( (fd = open("file.hole",O_RDWR|O_CREAT,S_IRUSR|S_IWUSR))<0 ) perror(argv[0]); if( write(fd,buf1,10) != 10 ) perror(argv[0]); if( lseek(fd,16384,SEEK_SET)==-1 ) perror(argv[0]); if( write(fd,buf2,10) != 10 ) perror(argv[0]); return 0; }
gcc -o test_createhole test_createhole.c ./test_createhole 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 ls -ls file.hole 8 -rw------- 1 16394 6月 15 14:53 file.hole
#include <fcntl.h> #include <stdio.h> char buf1[] = "abcdefghij"; char buf2[] = "ABCDEFGHIJ"; char buf3[] = "\0"; int main(int argc,char* argv[]) { int fd; int i; if( (fd = open("file.nohole",O_RDWR|O_CREAT,S_IRUSR|S_IWUSR))<0 ) perror(argv[0]); i = 0; while(i<16394){ if( write(fd,buf3,1) != 1 ) perror(argv[0]); i++; } if( lseek(fd,0,SEEK_SET)==-1 ) perror(argv[0]); if( write(fd,buf1,10) != 10 ) perror(argv[0]); if( lseek(fd,16384,SEEK_SET)==-1 ) perror(argv[0]); if( write(fd,buf2,10) != 10 ) perror(argv[0]); return 0; }运行结果如下:
gcc -o test_createnohole test_createnohole.c ./test_createnohole od -c file.nohole 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可以发现两个文件的内容完全相同,再来对比一下两个文件。
ls -ls file.hole file.nohole 8 -rw------- 1 16394 6月 15 14:53 file.hole 20 -rw------- 1 16394 6月 15 15:19 file.nohole
3.7 read函数,先来看看函数原型:
#include <unistd.h> ssize_t read(int fd,void* buf,size_t nbytes);返回值:读操作从当前的偏移量开始读,返回读到的字节数,若已到文件尾,返回0;若出错,返回-1。此处要注意read函数存在实际读到的字节数少于要求读的字节数。
3.8 write函数,还是先看看函数原型与返回值:
#include <unistd.h> ssize_t write(int fd,void* buf,size_t nbytes);
3.9节主要讨论I/O效率,在此就不给大家展开讲解了。
3.10节首先介绍了内核中I/O所用到的数据结构,在此给大家分享一篇blog吧,里面有些图,我就不盗用了。http://www.linuxidc.com/Linux/2015-01/111700.htm
在已有数据结构的基础上,结合之前介绍的操作做进一步说明:
APUE中还讨论了文件描述符和文件状态标志在作用范围方面的区别,前者只用于一个进程的一个描述符,而后者则应用于指向该给定文件表项的任何进程中的所有描述符(有可能是同一个进程中的不同文件描述符,若不同文件描述符共享文件表项,那么说明这些文件描述符共享文件状态标志、当前文件偏移量等)。
3.11节介绍了原子操作的相关概念,关于原子操作的例子,在上一小节的分析中已经给大家分享过了。
3.12节介绍了用于复制现有文件描述符的函数。函数原型如下:
#include <unistd.h> int dup(int fd); int dup2(int fd,int fd2);返回值:若成功,返回新的文件描述符;若出错,返回-1。
由dup返回的新文件描述符一定是当前可用文件描述符的最小数值,参数fd代表被复制的描述符,文件描述符之间不共享FD_CLOEXEC。对于dup2,可以用fd2参数指定新描述符的值,若fd2已经打开,则先将其关闭(这一过程我在内核源码中没有找到)。此时如若fd等于fd2,则dup2直接返回fd2。 否则(fd不等于fd2的情况下),fd2的FD_CLOEXEC文件描述符标志就被清除,如此fd2在进程调用exec时是打开状态。首先来看看FD_CLOEXEC的含义:“close on exec, not on-fork, 意为如果对描述符设置了FD_CLOEXEC,使用execl执行的程序里,此描述符被关闭,不能再使用它,但是在使用fork调用的子进程中,此描述符并不关闭,仍可使用”。但如果通过dup2函数对fd进行复制,fd2的FD_CLOEXEC标志位就被清除,此时fd2在进程调用exec时是打开状态。还是通过实验简单验证一下,以下的验证程序来自于这篇blog:http://blog.csdn.net/ustc_dylan/article/details/6930189
结合新学到知识验证以下,先来看一个有问题的例子:
#include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <string.h> int main(void) { int fd,pid; int newfd; char buffer[20]; fd=open("wo.txt",O_RDONLY); printf("%d\n",fd); int val=fcntl(fd,F_GETFD); val|=FD_CLOEXEC; fcntl(fd,F_SETFD,val); pid=fork(); if(pid==0) { //子进程中,此描述符并不关闭,仍可使用 char child_buf[2]; memset(child_buf,0,sizeof(child_buf) ); ssize_t bytes = read(fd,child_buf,sizeof(child_buf)-1 ); printf("child, bytes:%ld,%s\n\n",bytes,child_buf); //execl执行的程序里,此描述符被关闭,不能再使用它 char fd_str[5]; memset(fd_str,0,sizeof(fd_str)); sprintf(fd_str,"%d",fd); //直接使用原来的文件描述符,但此时没有清除FD_CLOEXEC标志,因此使用execl函数无法打开该文件描述符 int ret = execl("./exec1","exec1",fd_str,NULL); if(-1 == ret) perror("ececl fail:"); } waitpid(pid,NULL,0); memset(buffer,0,sizeof(buffer) ); ssize_t bytes = read(fd,buffer,sizeof(buffer)-1 ); printf("parent, bytes:%ld,%s\n\n",bytes,buffer); }
3 child, bytes:1,t exe1: read fail:: Bad file descriptor parent, bytes:14,his is a test
再来看看使用newfd的情况,同时newfd没有被初始化,源码如下:
#include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <string.h> int main(void) { int fd,pid; int newfd; char buffer[20]; fd=open("wo.txt",O_RDONLY); printf("%d\n",fd); int val=fcntl(fd,F_GETFD); val|=FD_CLOEXEC; fcntl(fd,F_SETFD,val); pid=fork(); if(pid==0) { //子进程中,此描述符并不关闭,仍可使用 char child_buf[2]; memset(child_buf,0,sizeof(child_buf) ); ssize_t bytes = read(fd,child_buf,sizeof(child_buf)-1 ); printf("child, bytes:%ld,%s\n\n",bytes,child_buf); //execl执行的程序里,此描述符被关闭,不能再使用它 char fd_str[5]; memset(fd_str,0,sizeof(fd_str)); printf("newfd = %d\n",newfd); if( (newfd = dup2(fd,newfd)) == -1 ) perror("dup2 fail:"); sprintf(fd_str,"%d",newfd); int ret = execl("./exec1","exec1",fd_str,NULL); if(-1 == ret) perror("ececl fail:"); } waitpid(pid,NULL,0); memset(buffer,0,sizeof(buffer) ); ssize_t bytes = read(fd,buffer,sizeof(buffer)-1 ); printf("parent, bytes:%ld,%s\n\n",bytes,buffer); }
上述程序中newfd并没有初始化,此处比较凑巧,在子进程中被初始化为0(此处先存下一个疑问,每次运行的时候都是0,没有初始化的临时变量应该是随机值)。由于我的newfd初始值是0,根据dup2函数的功能:“如果fd2已经被打开,则先将其关闭”,此时文件描述符0就被关闭。而后调用dup2函数清除FD_CLOEXEC标志位,并将文件描述符0重定向至fd(wo.txt),调用execl函数后使用的是已经打开的文件描述符0。
运行结果如下:
3 child, bytes:1,t newfd = 0 exe1: read 14,his is a test parent, bytes:0,
改为4试试,程序再次正常运行。若改为使用dup函数,newfd值为4,同时程序正常运行。
有关于dup、dup2函数的源码分析请见:http://blog.csdn.net/u012927281/article/details/51711085
虽然两个不同的文件描述符具有相同的文件状态标志以及同一当前文件偏移量,但每个文件描述符都有它自己的一套文件描述符标志(close_on_exec),新描述符的执行时关闭(close_on_exec)标志总是由dup函数清除。
3.13 sync、fsync、fdatasync函数,先来看看这几个函数的原型:
#include <unistd.h> int fsync(int fd); int fdatasync(int fd); void sync(void);前两个函数的返回值为:若成功,返回0;若出错,返回-1。内核中设有缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。由于内存与磁盘写入速度上的差距,当我们向文件写入数据时,内核通常先将数据写入缓冲区,然后排入队列,晚些时候再写入磁盘。上述方式被称为延迟写。在需要重用缓冲区来存放其他磁盘块数据时,它会把所有延迟写数据块写入磁盘。延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,使得欲写到文件中的数据在一段时间内并没有写到磁盘上。当系统发生故障时,这种延迟可能造成文件更新内容的丢失。为了保证磁盘上实际文件系统与缓冲区中内容的一致性,就用到了上述三个函数。
sync的作用是将所有修改过的块缓冲区排入写队列,然后就返回,它不等待实际写磁盘操作结束。通常,由update(守护进程)周期性地调用sync函数。这就保证了定期冲洗(flush)内核的块缓冲区。命令sync也调用sync函数。fsync还会同步更新文件的属性(metadata,包括size、访问时间st_atime&st_mtime等等)。
fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。fdatasync函数类似于fsync,但它只影响文件的数据部分。
还是给大家分享一篇blog,书中的内容有些不全面:http://blog.csdn.net/cywosp/article/details/8767327
3.14 fcntl函数可以改变已经打开文件的属性
#include <fcntl.h> int fcntl(int fd,int cmd,.../*int arg*/);返回值:若成功,则依赖于cmd;若出错,返回-1。
fcntl函数有以下5种功能。
关于文件描述标志与文件状态标志的区别,请见这篇blog:http://blog.csdn.net/hittata/article/details/8665892
最后两章就不给大家分享什么了,现在看可能还用不到,以后用到的时候再说吧。
这里关于习题3.5还要再谈一点,习题如下:
./a.out > outfile 2>&1 ./a.out 2>&1 > outfile首先根据题目中的提示digit1>&digit2表示要将描述符digit1重定向至描述符digit2,通过前面的介绍我们已经知道要实现描述符的重定向,也就是让两个不同的文件描述符指向同一个文件表项,这一功能可通过dup/dup2函数实现。好了背景知识总结到这里,接下来详细看看这两条命令有什么区别。
./a.out > outfile 2>&1首先将标准输出重定向到outfile文件,接下来通过dup函数,使文件描述符2指向文件描述符1指向的文件表项,则此时文件描述2也指向outfile文件。
./a.out 2>&1 > outfile首先通过dup函数,将文件描述符1复制到文件描述符2,则此时文件描述2指向文件描述1指向的文件表项,而后将文件描述符1重定向到outfile文件,这一操作过后,文件描述符2指向文件描述符1原来指向的文件表项,文件描述符1指向outfile的文件表项。