1. linux 环境中的文件具有特别重要的意义,因为它们为操作系统服务和设备提供了一个简单而统一的接口。在 linux 中,一切(或几乎一切)都是文件。这就意味着,通常程序完全可以像使用文件那样使用磁盘文件、串行口、打印机和其他设备。
2. 目录也是文件,但它是一种特殊类型的文件。在现代的 UNIX(包括 linux )版本中,即使是超级用户可能也不再允许直接对目录进行写操作。正常情况下,所有用户都必须用上层的 opendir/readdir 接口来读取目录,而无需了解特定系统中目录实现的具体细节。
3. 文件,除了本身包含的内容以外,还会有一个名字和一些属性,即“管理信息”,例如文件的创建/修改日期和它的访问权限等。这些属性被保存在文件的 inode(节点)数据结构中,它是文件系统中的一个特殊的数据块,它同时还包含文件的长度和文件在磁盘上的存放位置。
4. 目录是文件,它用来保存其他文件的节点号和名字。目录文件中的每个数据项都指向某个文件的节点,删除文件名就等于删除与之对应的链接(文件的节点号可以通过 ln -i 命令查看)。可以通过使用 ln 命令在不同的目录中创建指向同一个文件的链接。如果指向某个文件的链接数(即 ls -l 命令的输出中跟在访问权限后面的数字)变为零,就表示该节点以及其指向的数据不再被使用,磁盘上的相应位置就会被标记为可用空间。
5. ~user(~加用户名) 表示用户的主目录文件夹,注意,标准库函数不能识别文件名参数中的波浪线符号。
6. UNIX 和 linux 中比较重要的设备文件有三个——/dev/console、/dev/tty 和 /dev/null 。
/dev/console:这个设备表示的是系统控制台。错误信息和诊断信息通常会被发送到这个设备。每个 UNIX 系统都会有一个指定的终端或显示屏用来接受控制台的消息。
/dev/tty : 如果一个进程有控制终端的话,那么特殊文件 /dev/tty 就是这个控制终端(键盘和显示屏,或键盘和窗口)的别名(逻辑设备)。在能够使用该设备文件的情况下, /dev/tty 允许程序直接向用户输出信息,而不管用户具体使用的是哪种类型的伪终端或硬件终端。在标准输出被重定向时,这一功能非常有用。命令 ls -R | more 就是这样一个例子,more 程序需要提示用户进行键盘操作之后才能显示下一页内容。
/dev/null: 这事空(null)设备。所有写向这个设备的输出都将被丢弃。而读这个设备会立刻返回一个文件尾标志,所以在 cp 命令里可以把它用作拷贝空文件的源文件。人们常把不需要的输出重定向到 /dev/null 。
7. 创建空文件的另一个方法是使用 touch <filename> 命令,它的作用是改变文件的修改时间,如果指定名字的文件不存在,就创建一个新文件。但它并不会把有内容的文件变成空文件。
8. 可以在 /dev 目录中找到的其他设备包括,硬盘和软盘、通信端口、磁带驱动器、CD-ROM、声卡以及一些代表系统内部工作状态的设备。甚至还有 /dev/zero 设备,它的作用是作为内容是 null 字节的源文件来创建零长度文件。访问其中的某些设备需要具有超级用户权限,普通用户不能通过编写程序直接访问如硬盘这样的底层设备。
9. 设备可分为字符设备和块设备。两者区别在于访问设备时是否需要一次读写一整块。一般情况下,块设备是那些支持随机文件系统存取的设备,例如硬盘。
10. 为了向用户提供一个统一的接口,设备驱动程序封装了所有与硬件相关的特性。硬件的特有功能可以通过 ioctl 调用来完成。 /dev 目录中的设备文件的用法都是一致的,它们都可以被打开、读、写和关闭。例如,用来访问普通文件的 open 调用同样可以用来访问一台用户终端、一台打印机或一台磁带机。用来访问设备驱动程序的底层函数(系统调用)包括:
open :打开文件或设备。
read : 从打开的文件或设备里读数据。
write: 向文件或设备写数据。
close:关闭文件或设备。
ioctl :把控制信息传递给设备驱动程序。
11. 系统调用 ioctl 用来提供一些与特定硬件设备有关的必要控制(与正常输入输出相反),所以它的用法随设备的不同而不同。因此, ioctl 并不需要具备可移植性。每个驱动程序都定义了它自己的一组 ioctl 命令。
12. 在输入输出操作中,直接使用底层系统调用的问题是它们的效率非常低。
系统调用会影响系统的性能。与函数调用相比,系统调用的开销要大些,因为在执行系统调用时,linux 必须从用户代码切换到内核代码运行,然后再返回用户代码。减少这种开销的一个好方法是,在程序中尽量减少系统调用的次数,并且让每次系统调用完成尽可能多的工作。例如每次读写大量的数据,而不是每次仅读写一个字符。
硬件会对底层系统调用一次所能读写的数据块做出一定的限制。例如,磁带机通常的写操作数据块长度是 10K ,所以如果所写的数据量不是 10K 的整数倍,磁带机还是会以 10K 为单位卷绕磁带,这就在磁带上留下的空隙。
13. 每个运行中的程序被称为进程 (process) ,它有一些与之关联的文件描述符。这是一些小值整数,你可以通过它们访问打开的文件或设备。有多少我饿那件描述符可用取决于系统的配置情况。当开始运行程序时,它一般会有三个已经打开的文件描述符。它们是 :
0:标准输入
1:标准输出
2:标准错误
14. write 系统调用:作用是把缓冲区 buf 的前 nbytes 个字节写入与文件描述符 fildes 关联的文件中。
它返回实际写入的字节数。如果文件文件描述符有错或者底层的设备驱动程序对数据块长度比较敏感,该返回值可能会小于 nbytes 。如果这个函数的返回值是 0 ,就表示未写入任何数据,如果是 -1, 就表示在write 调用中出现了错误,对应的错误代码保存在全局变量 errno 里面。
函数原型:
#include <unistd.h> size_t write(int fildes, const void *buf, size_t nbytes);
实例:
#include <unistd.h> #include <stdlib.h> int main(){ if((write(1, "Here is some data\n",18)) != 18) write(2,"A write error has occurred on file descriptor 1\n",46); exit(0); }
这个程序只是在标准输出上显示一条消息。当程序退出运行时,所有打开的文件描述符都会自动关闭,所以我们不需要明确地关闭它们。但当我们在处理被缓冲的输出时,情况就不一样了。
需要再次提醒的是,write 可能会报告说它写入的字节比你要求的少。这并不一定是个错误。在程序中,你需要检查 errno 以发现错误,然后再次调用 write 写入剩余的数据。
15. read 系统调用:作用是从与文件描述符 fildes 相关联的文件里读入 nbytes 个字节的数据,并把它们放到数据去 buf 中。
它返回实际读入的字节数,它可能会小于请求的字节数。如果 read 调用返回 0 ,那就表示未读入任何数据,已经到达了文件尾。同样,如果返回 -1 ,就表示 read 调用出现了错误。
函数原型:
#include <unistd.h> size_t read(int fildes, void *buf, size_t nbytes);
实例:
/* 这个程序把标准输入的前 128 个字节拷贝到标准输出。 * 如果输入少于 128 个字节,就把它们全体拷贝过去。 */ #include <unistd.h> #include <stdlib.h> int main(){ char buffer[128]; int nread; nread = read(0, buffer, 128); if (nread==-1) write(2, "A read error has occurred\n",26); if((write(1, buffer, nread)) != nread) write(2, "A write errot has occurred\n",27); exit(0); }
16. open 系统调用: 创建新的文件描述符需要使用系统调用 open。
函数原型:
#include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> int open(const char *path, int oflags); int open(const char *path, int oflags, mode_t mode);
严格来说,在符合 POSIX 规范的系统上,使用 open 系统调用并不需要包括头文件 sys/types.h 和 sys/stat.h,但在某些 UNIX 系统上它们可能是必不可少的。
open 建立了一条到文件或设备的访问路径。如果操作成功,它将返回一个文件描述符,read 和 write 等系统调用使用该文件描述符就可以对打开的文件进行操作。这个文件描述符是唯一的,它不会与任何其他运行中的进程共享。如果两个程序同时打开一个文件,会得到两个不同的文件描述符。如果它们都对文件进行写操作,它们会各写各的,分别接着上次离开的位置继续往下写。它们的数据不会交织在一起,而是彼此相互覆盖(后写入的内容覆盖掉前面切入的内容)。两个程序对文件的读写位置(偏移值)有各自的理解。为了防止这种我们不希望看到的冲突局面,可以使用“文件锁”功能。
path:准备打开的文件或设备的名字
oflags:定义打开文件所采取的动作,必须指定下列访问模式之一
O_RDONLY 以只读方式打开
O_WRONLY 以只写方式打开
O_RDWR 以读写方式打开
还可以在 oflags 参数中包含下列可选模式(用“按位或”操作)
0_APPEND:把写入的数据追加在文件的末尾
O_TRUNC:把文件长度设置为零,丢弃已有的内容
O_CREAT:如果需要,就按参数 mode 中给出的访问模式创建文件
O_EXCL:与 O_CREAT 一起使用,确保调用者创建出文件。open 是一 个原子操作,使用这个可选模式可以防止两个程序同时创建一个文 件。如果文件已经存在,open 调用将失败。
open 调用在成功时返回一个新的文件描述符(非负数),失败时返回 -1,并设置全局变量 errno 以指明失败的原因。新文件描述符总是使用未用描述符的最小值。例如,如果一个程序关闭了自己的标准输出,然后再次调用 open ,文件描述符就会被重新使用,并且标准输出将被有效地重定向到另一个文件或设备。
当带有 O_CREAT 标志的 open 来创建文件时,我们必须使用有三个参数格式的 open 调用。第三个参数 mode 是几个标志按位 OR 后得到的,这些表示在头文件 sys/stat.h 中定义,它们是:
S_IRUSR:读权限,文件属主
S_IWUSR:写权限,文件属主
S_IXUSR:执行权限,文件属主
S_IRGRP:读权限,文件所属组
S_IWGRP:写权限,文件所属组
S_IXGRP:执行权限,文件所属组
S_IROTH:读权限,其他用户
S_IWOTH:写权限,其他用户
S_IXOTH:执行权限,其他用户
例子:
open("myfile", O_CREAT, S_IRUSR | S_IXOTH);
它的作用是创建一个名为 myfile 的文件,文件属主拥有读权限,其他用户拥有执行权限,且只有这些权限。
-r-------x 1 dmx dmx 0 1月 2 13:06 myfile
有几个因素会对文件的访问权限产生影响:
只有在创建文件时才会指定访问权限。
用户掩码(由 shell 的 umask 命令设定)会影响到被创建文件的访问权限open调用里给出的模式值将与当时的用户掩码的反值做 AND 操作。例如:如果用户掩码被设置为 001 ,并指定了 S_IXOTH 模式标志,那么其他用户对创建的文件不会拥有执行权限,因为用户掩码中制定了不允许向其他用户提供执行权限。
因此,open 和 creat 调用中的标志实际上是是指文件访问权限的请求,所请求的权限是否会被设置取决于当时 umask 的值。
17. umask 变量是一个系统变量:当文件被创建时,为文件的访问权限设定一个掩码。执行 umsk 命令可以对这个变量进行修改,为其提供一个新值。(欲知umask的值的意义http://baike.baidu.com/view/1867757.htm)
18. close 系统调用:终止一个文件描述符 fildes 与其对应文件之间的关联。文件描述符被释放并能够重新使用。
close 调用成功就返回 0 ,出错就返回 -1 。
函数原型:
#include <unistd.h> int close(int fildes);
注意:有时检查 close 调用的返回结果十分重要。有的文件系统,特别是网络文件系统,可能不会在关闭文件之前报告文件写操作中出现的错误,因为执行写操作时,数据可能未被确认写入。
19. 运行中的程序能够一次打开的文件数目是有限制的。这个限制由头文件 limits.h 中的OPEN_MAX 常数定义。
20. ioctl 系统调用:提供了一个用于控制设备及其描述符行为和配置底层服务的接口。
函数原型:
#include <unistd.h> int ioctl (int fildes, int cmd, ...);
实例:
/*一个文件拷贝程序 */ #include <unistd.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> int main(){ char c; int in, out; in = open("file.in", O_RDONLY); out = open("file.out", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR); while(read(in, &c, 1) == 1) write(out,&c,1); exit(0); }
注意:#include <unistd.h> 必须最早出现,因为它根据 POSIX 规范定义的标志可能会影响到其他的头文件。
我们需要 file.in 文件,我用了一个 4.6M 的文本文件,执行时间为
$ time ./copy_system
real 0m7.970s
user 0m0.280s
sys 0m7.676s
之所以这么慢,是因为它必须完成超过八百万次的系统调用。
我们可以通过拷贝大一些的数据块来改善效率低的问题,下面是改进程序,它每次拷贝 1K 的数据块,用的还是系统调用:
/*一个文件拷贝程序 */ #include <unistd.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> int main(){ char block[1024]; int in, out; int nread; in = open("file.in", O_RDONLY); out = open("file.out", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR); while((nread = read(in, block, sizeof(block))) > 0) write(out,block,nread); exit(0); }
先 rm file.out, 然后
$ time ./a.out
real 0m0.027s
user 0m0.000s
sys 0m0.028s
毫无疑问,效率差距显著,因为只需要做几千次系统调用。当然,这些时间与系统本身的性能有很大关系,但它们确实显示了系统调用需要巨大的开支,因此值得对其进行优化。
21. fopen 函数:用于文件和终端的输入输出。如果需要对设备的行为进行明确的控制,那最好使用底层系统调用,因为这可以避免用库函数带来的一些非预期的潜在副作用,如输入/输出缓冲。
函数原型:
#include <stdio.h> FILE *fopen(const char *filename, const char *mode);
fopen 打开由 filename 参数指定的文件,并把它与一个文件流关联起来。mode 参数指定文件的打开方式,为下列字符串之一:
“r” 或“rb”:以只读方式打开。
“w”或“wb”:以写方式打开,并把文件长度截短为零
“a” 或“ab”:以写方式打开,新内容追加在文件尾
“w+”或“w+b”或“wb+”:以修改方式打开,并把文件长度截短为零
“r+” 或“r+b”或“rb+”:以修改方式打开(读和写)
“a+”或“a+b“或”ab+“:以修改方式打开,新内容追加在文件尾
字母 b 表示文件是一个二进制文件而不是文本文件。注意:mode 参数必须是字符串,而不是字符,所以总是使用“r”,而不是“r”。
22. fread 函数:从一个文件流里读取数据。对数据记录进行操作。
函数原型:
#include <stdio.h> size_t fread (void *ptr, size_t size, size_t nitems, FILE *stream);
数据从文件流 stream 读到由 ptr 指定的数据缓冲区里
size参数记录每个数据记录的长度
计数器 nitems 给出要传输的记录个数
它的返回值是成功地读到数据缓冲区里的记录个数(而不是字节数)。当到达文件尾时,它的返回值可能会小于 nitems,甚至可以是零。
23. fwrite 函数:从指定的数据缓冲区里取出数据记录,并把它们写到输出流中。对数据记录进行操作。
它的返回值是成功写入的记录个数。
函数原型:
#include <stdio.h> size_t fwrite (const void *ptr, size_t size, size_t nitems, FILE *stream);
参数和 fread 基本一致。
24. fclose 函数:关闭指定的文件流 stream,使所有尚未写出的数据都写出。
因为 stdio 库会对数据进行缓冲,所以使用 fclose 是很重要的。如果程序需要确保数据已经全部写出,就应该调用 fclose 函数。与文件描述符一样,可用文件流的数目也是有限制的。这个限制由头文件 stdio.h 中的 FOPEN_MAX 常量定义,最小为 8.
函数原型:
#include <stdio.h> int fclose(FILE *stream)
25. fflush 函数:把文件流里所有未写出的数据立刻写出。
函数原型:
#include <stdio.h> int fflush(FILE *stream);
26. 另一个文件拷贝函数
/*一个文件拷贝程序 * 运用系统调用 */ #include <stdlib.h> #include <stdio.h> int main(){ int c; FILE *in, *out; in = fopen("file.in","r"); out = fopen("file.out", "w"); while((c=fgetc(in)) != EOF) fputc(c,out); exit(0); }
结果:
$ time ./a.out
real 0m0.123s
user 0m0.104s
sys 0m0.016s
不如底层数据库拷贝版块快,但比那个一次拷贝一个字符的版块要快多了。因为 stdio 库在FILE结构里使用了一个内部缓冲池,之后在缓冲区满时才进行底层系统调用。
27. 我们可以用 unlink 系统调用来删除一个文件。unlink 系统调用删除一个文件的目录项并减少它的链接数。成功返回0,失败返回1。如果一个文件的链接数减少到零,并且没有进程打开它,这个文件就会被删除。实际情况是,目录项总是先被删除,但文件所占用的空间要等到最后一个进程(如果有的话)关闭它之后才会被系统回收。我们可以使用 link 系统调用在程序中创建一个文件的新链接。
先用 open 创建一个文件,然后对其调用 unlink 是某些程序员用来创建临时文件的技巧。这些文件只有在被打开的时候才能被程序使用,当程序退出文件关闭的时候,它们就会被自动地删除掉。
28. 还有许多系统调用函数,http://blog.csdn.net/rootsongjc/article/details/7062694,想要了解,可以阅读专门的书籍或者网上搜索。
声明:
本文为 iddmx 对《Beginning Linux Programming》(Third Edition) Chapter 3 的读书笔记。内容大部分都是书中内容,也有一些个人理解,由于 iddmx 水平有限,书中许多精华没能提取出来,如果想要深入了解,请阅读原书。
本文欢迎自由转载,但请务必保持本文完整或注明来之本文。本文未经 iddmx 同意,不得用于商业用途。最后,如果您能从这个简单文档里获得些许帮助,iddmx 将对自己的一点努力感到非常高兴;iddmx 水平有限,如果本文中包含的错误给您造成了不便,iddmx 在此提前说声抱歉,并希望您能j将错误告知 iddmx([email protected]),以便修正。
祝身体健康,工作顺利。☺