系列文章:
文件和设备、系统调用、库函数、底层文件访问、管理文件、标准I/O库、格式化输入和输出、文件和目录的维护、扫描目录、错误及其处理、/proc文件系统、fcntl、mmap
Linux 文件结构
目录
文件,除了本身包含的内容,还会有一个名字和一些属性,即管理信息,包括文件的创建/修改日期和它的访问权限。这些属性被保存在文件的 inode 节点中。它是文件系统中的一个特殊的数据块,它同时还包含文件的长度和文件在磁盘上的存放位置。系统使用的是文件的 inode 编号。
目录,用于保存其它文件的节点号和文件名的文件。目录文件中的每个数据项都是指向某个文件节点的链接,删除文件名等于删除与之对应的链接。
删除一个文件时,实质是删除了该文件对应的目录项,同时指向该文件的链接数减1,该文件中的数据可能仍然能够通过其它指向同一文件的链接访问到。如果指向某个文件的链接数(即 ls -l 命令的输出中跟在访问权限后面的那个数字)变为零,就表示该节点以及其指向的数据不再被使用,磁盘上的相应位置就会被标记为可用空间。
- /bin 存放系统程序(二进制可执行文件)
- /etc 存放系统配置文件
- /lib 系统函数库
- /dev 物理设备及其接口文件
文件和设备
甚至硬件设备在 linux 中通常也被表示为文件。
sudo mount -t iso9660 /dev/hdc /mnt/cdrom
cd /mnt/cdrom
UNIX 和 Linux 中比较重要的设备文件:
/dev/console
这个设备代表的是系统控制台。错误信息和诊断信息通常都会被发送到这个设备。
/dev/tty
如果一个进程有控制终端的话,那么特殊文件、dev/tty 就是这个控制终端的别名。例如,由系统自动运行的进程和脚本就没有控制终端,所以它们不能打开 /dev/tty。
在能够使用该设备文件的情况下,/dev/tty 允许程序直接向用户输出信息,而不管用户具体使用的是哪种类型的伪终端或硬件终端。在标准输出被重定向时,这一功能非常有用。使用命令 ls -R | more 显示一个长目录列表就是这样一个例子,more 程序需要提示用户进行键盘操作之后才能显示下一页的内容。
/dev/null
空设备,所有写向这个设备的输出都将被丢弃,而读这个设备立刻返回一个文件尾标志。人们常常把不重要的输出重定向到该文件。
echo do not want to see this >/dev/null
cp /dev/null empty_file
系统调用和设备驱动程序
你只需要很少量的函数就可以对文件和设备进行访问和控制,这些函数称为系统调用,它们也是通向操作系统本身的接口。
操作系统的核心部分,即内核,是一组设备驱动程序,它们是一组对系统硬件进行控制的底层接口。为了向用户提供一个一致的接口,设备驱动程序封装了所有与硬件相关的特性。硬件的特有功能通常可通过 ioctl(用于I/O控制) 系统调用来提供。
/dev 目录中的设备文件的用法都是相同的,它们都可以被打开、读、写和关闭。例如,用来访问普通文件的 open 调用同样可以用来访问用终端、打印机或者磁带机。与之相关的系统调用有open 、read 、write 、close 、ioctl ,系统调用ioctl用于提供一些与特定硬件设备有关的必要控制(与正常的输入输出相反),所以它的用法随设备的不同而不同,因此,ioctl 并不需要具备可移植性。此外,每个驱动程序都定义了自己的一组 ioctl 命令。
库函数
针对输入输出操作直接使用底层系统调用的问题是效率比较低,为什么?
- 使用系统调用会影响系统的性能。与函数调用相比,系统调用的开销要大一些,因为在执行系统调用时,Linux必须从用户代码切换到内核代码,然后再返回用户代码。减少这种开销的一个方法是,在程序中尽量减少系统调用的次数,并且让每次系统调用都尽可能完成更多的工作。
- 硬件会限制对底层系统调用一次所能读写的数据块大小。
为了给设备和磁盘文件提供更高层的接口,Linux 提供了一系列的标准函数库。它们是一些由函数构成的集合,你可以把它们应用到自己的程序中,比如提供输出缓冲功能的标准I/O库。你可以高效地写任意长度的数据块,库函数则在数据满足数据块长度要求时安排执行底层系统调用这就极大降低了系统调用的开销。
底层文件访问
每个运行中的程序被称为进程,它有一些与之关联的文件描述符。这是一些小值整数,你可以通过它们访问打开的文件或设备。有多少文件描述符可用取决于系统的配置情况。当一个程序开始运行时,它一般会有3个已经打开的文件描述符:0、1、2。你可以通过系统调用open把其它文件描述符与文件和设备相关联。
write 系统调用
#include<unistd.h>
size_t write(int fildes, const void *buf, size_t nbytes);
把缓冲区buf的前nbytes个字节写入与文件描述符fildes关联的文件中。返回实际写入的字节数。如果文件描述符有错误或者底层的设备驱动程序对数据块长度比较敏感,该返回值可能小于 nbytes。如果函数返回 0,就表示未写入任何数据,如果返回-1,表示系统调用发生错误,错误代码保存在全局变量 errno 里面。
#include <unistd.h>
#include <stdlib.h>
int main()
{
if((write(1, "Here is some data\n", 18)) != 18)
write(2, "A write error has occured on file descriptor 1\n", 46);
exit(0);
}
这个程序只是在标准输出上显示一条信息,当程序退出运行时,所有已经打开的文件描述符会自动关闭,所以不需要明确关闭它们。
write 可能会报告写入的字节比要求的少,但并不一定是一个错误,在程序中,需要检查 errno 以发现错误,然后再次调用 write 写入剩余的数据。
read 系统调用
#include <unistd.h>
size_t read(int fildes, void *buf, size_t nbytes);
与从文件描述符 fildes 相关联的文件里读入 nbytes 个字节的数据,并把它们放到数据区 buf 中,它返回实际读入的字节数,这可能会小于请求的字节数。如果 read 调用返回 0,就表示未读入任何数据,已到达了文件尾,如果返回 -1,就表示 read 调用出现了错误。
#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 occured\n", 26);
if((write(1, buffer, nread)) != nread)
write(2, "A write error has occured\n", 27);
exit(0);
}
echo hello there | ./a.out
./a.out < draft1.txt
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);
简单来说,open 建立了一条到文件或设备的访问路径,如果调用成功,它将返回一个可以被 read、write 和其它系统调用使用的文件描述符。这个文件描述符是唯一的,它不会与任何其他运行中的进程共享。==如果两个程序同时打开同一个文件,它们会分别得到两个不同的文件描述符。==如果它们都对文件进行写操作,那么它们会各自写各自的,它们分别接着上次离开的位置继续往下写。它们的数据不会交织在一次,而是彼此互相覆盖。两个程序对文件的读写位置不同。可以通过使用文件锁功能来防止出现冲突。
准备打开的文件或设备的名字作为参数 path 传递给函数,oflags 参数用于指定打开文件所采取的的动作。oflags 参数是通过必需文件访问模式与其它可选模式相结合的方式来指定。
模式 | 说明 |
---|
O_RDONLY | 只读方式 | O_WRONLY | 只写方式 | O_RDWR | 读写方式 |
open 调用还可以在 oflags 参数中包括下列可选模式的组合(按位或操作)。
- O_APPEND 把写入数据追加在文件的末尾
- O_TRUNC 把文件长度设置为零,丢弃已有的内容
- O_CREATE 如果需要,就按参数 mode 中给出的访问模式创建文件
- O_EXCL 与 O_CREAT 一起使用,确保调用者创建出文件,Open调用是一个原子操作,它只执行一个函数调用,使用这个可选模式可以防止两个程序同时创建同一个文件。
open 调用在成功之后返回一个新的文件描述符(总是一个非负整数),在失败时返回 -1 并设置全局变量 errno 来指明失败的原因。
任何一个运行中的程序能够同时打开的文件数目是有限制的,这个限制通常是由 limits.h 头文件中的常量 OPEN_MAX 定义的,它的值随系统的不同而不同,但 POSIX 要求它至少为 16,它通常一开始被设置为 256。
访问权限的初始值
当使用带有 O_CREAT 标志的 open 调用来创建文件时,必须使用有 3 个参数格式的 open 调用。第三个参数 mode 是几个标志按位或后得到的,这些标志在头文件 sys/stat.h 中定义。
- S_IRUSR 读权限,文件属主
- S_IWUSR 写权限 文件属主
- S_IXUSR 执行权限 文件属主
- S_IRGRP 读权限 文件所属组
- S_IWGRP 写权限 文件所属组
- S_IXGRP 执行权限 文件所属组
- S_IROTH 读权限 其他用户
- S_IWOTH 写权限 其他用户
- S_IXOTH 执行权限 其他用户
有几个因素会对文件的访问权限产生影响,首先,指定的访问权限只有在创建文件时才会使用,其次用户掩码会影响到创建文件的访问权限。
open 调用里给出的 mode 值将与用户掩码的反值做 AND 操作。如果umask=001,指定S_IXOTH,那么其它用户对创建的文件不会拥有执行权限,==因为用户掩码指定了不允许向其他用户提供执行权限。==因此,open 和 creat 调用中的标志实际上是发出设置文件访问权限的请求,所请求的权限是否会被设置取决于当时的umask值。
umask
是一个系统变量,作用是:当文件被创建时,为文件的访问权限设定一个掩码,执行 umask 命令可以修改,是一个由3个八进制数字组成的值。
禁止组的写和执行权限:
xxx xxx xxx xxx x11 x1x 000 011 010 umask = 032
即使创建该文件的系统调用要求拥有其它用户的写权限,也不被允许,但程序随后可以使用 chmod 命令来添加权限。
close 系统调用
int close(int fildes);
使用 close 调用终止文件描述符 fildes 与其对应文件之间的关联。文件描述符被释放并能够重新使用。close 调用成功时返回 0,出错时返回 -1。
ioctl 系统调用
它提供了一个用于控制设备及其描述符行为和配置低层服务的接口。终端、文件描述符、套接字甚至磁带机都可以有为它们定义的 ioctl。
int ioctl(int fildes, int cmd, ...);
ioctl 对描述符 fildes 引用的对象执行 cmd 参数中给出的操作。根据特定设备所支持的操作的不同,它还可能会有一个可选的第三参数。
对一个 10M 字符文本的读写
#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);
}
使用 time 工具对程序的运行时间进行测算。Linux 使用 TIMEFORMAT 变量来重置默认的 POSIX 时间输出格式,POSIX 时间格式不包括 CPU 的使用率,之所以这么慢,是因为它必须完成超过两千万次系统调用。
jiaming@jiaming-pc:~/Documents/test$ TIMEFORMAT="" time ./a.out
8.88user 26.98system 0:35.98elapsed 99%CPU (0avgtext+0avgdata 1124maxresident)k
20480inputs+20488outputs (0major+56minor)pagefaults 0swaps
如果不单字符进行复制,使用下面的 1K 数据块形式整体复制,只需要 2K 次系统调用,运行时间大大减少。
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
char c[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, &c, sizeof(c))) > 0)
{
write(out, &c, nread);
}
exit(0);
}
jiaming@jiaming-pc:~/Documents/test$ TIMEFORMAT="" time ./a.out
0.01user 0.05system 0:00.06elapsed 98%CPU (0avgtext+0avgdata 1072maxresident)k
24inputs+24outputs (0major+57minor)pagefaults 0swaps
lseek 系统调用
lseek 系统调用对文件描述符 fildes 的读写指针进行设置。可以用它来设置文件的下一个读写位置。读写指针可以是绝对位置,也可以是相对于当前位置或文件尾的相对位置。
#include <unistd.h>
#include <sys/types.h>
off_t lseek(int fildes, off_t offset, int whence);
offset 参数用来指定位置,whence 定义偏移值用法,whence 可以取以下值:
- SEEK_SET: offset 是一个绝对位置;
- SEEK_CUR: offset 是相对于当前位置的一个相对位置;
- SEEK_END: offset 是相对于文件尾的一个相对位置;
lseek 返回从文件头到文件指针被设置处的字节偏移值,失败时返回 -1。
fstat、stat 和 lstat 系统调用
fstat 系统调用返回与打开的文件描述符相关的文件的状态信息,该信息会写到一个 buf 结构中,buf 的地址以参数形式传递给 fstat。
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
int fstat(int fildes, struct stat *buf);
int stat(const char *path, struct stat *buf);
int lstat(const char *path, struct stat *buf);
相关函数 stat 和 lstat 返回的是通过文件名查到的状态信息,它们产生相同的结果,但当文件是一个符号链接时,lstat 返回的是该符号链接本身的信息,而 stat 返回的是该链接指向的文件的信息。
stat 结构的成员:
st_mode: 文件权限和文件类型信息
st_ino: 与该文件关联的inode
st_dev: 保存文件的设备
st_uid: 文件属主的UID号
st_gid: 文件属主的GID号
st_atime: 文件上一次被访问的时间
st_ctime: 文件的权限、属主、组或内容上一次被改变的时间
st_mtime: 文件的内容上一次被修改的时间
st_nlink: 该文件上硬链接的个数
stat 结构中返回的 st_mode 标志还有一些与之关联的宏,它们定义在头文件 sys/stat.h 中。这些宏包括对访问权限、文件类型标志以及一些用于帮助测试特定类型和权限的掩码的定义。
访问权限标志与 open 系统调用中的内容一致,文件类型标志如下:
S_IFBLK: 文件是一个特殊的块设备
S_IFDIR:
S_IFCHR:
S_IFIFO:
S_IFREG: 文件是一个普通文件
S_FLNK: 文件是一个符号链接
其它模式标志:
S_ISUID: 文件设置了 SUID 位
S_ISGIID: 文件设置了 SGID 位
下面列出了用于解释 st_mode 标志的掩码:
S_IFMT: 文件类型
S_IRWXU: 属主的读/写/执行权限
S_IRWXG: 属组的读/写/执行权限
S_IRWXO: 其他用户的读/写/执行权限
下面是一些用来帮助确定文件类型的宏定义,它们对经过掩码处理的模式标志和相应的设备类型标志进行比较。
S_ISBLK: 测试是否是特殊的块设备文件
S_ISCHR: 测试是否是特殊的字符设备文件
S_ISDIR
S_ISFIFO
S_ISREG
S_ISLNK
举例,测试一个文件是不是目录,并且设置了属主的执行权限,并且不再有其它权限。
struct stat statbuf;
mode_t modes;
stat("filename", &statbuf);
modes = statbuf.st_mode;
if(!S_ISDIR(modes) && (modes & S_IRWXU) == S_IXUSR)
dup 和 dup2 系统调用
dup 系统调用提供了一种复制文件描述符的方法,使我们能够通过两个或者更多的不同的描述符来访问同一文件。这可以用于在文件的不同位置对数据进行读写。dup 系统调用复制文件描述符 fildes,返回一个新的描述符。dup2 系统调用则是通过明确指定目标描述符来把一个文件描述符复制为另一个。
#include <unistd.h>
int dup(int fildes);
int dup2(int fildes, int fildes2);
当通过管道在多个进程间进行通信时,这些调用很有用。
标准 I/O 库
标准 I/O 库及其头文件(stdio.h)为底层 I/O 系统调用提供了一个通用的接口。这个库现在已经称为 ANSI 标准 C 的一部分,而前面的系统调用却还不是。标准 I/O 库提供了许多复杂的函数用于格式化输出和扫描输入,还负责满足设备的缓冲需求。
在很多方面,你使用标准 I/O 库的方式和使用底层文件描述符一样,你需要先打开一个文件以建立一个访问路径。这个操作的返回值将作为其它 I/O 库函数的参数。在标准 I/O 库中,与底层文件描述符对应的是流,它被实现为指向结构 FILE 的指针。
在启动程序时,有3个文件流是自动打开的。它们是 stdin、stdout和stderr。它们都是在 stdio.h 头文件里定义的,分别代表着标准输入、标准输出和标准错误输出。与底层文件描述符0、1和2相对应。
fopen 函数
fopen 库函数类似于底层的 open 系统调用。主要用于文件和终端的输入输出。==如果你需要对设备进行明确的控制,那最好使用底层系统调用。==因为这可以避免用库函数带来的一些潜在问题,如输入/输出缓冲。
#include <stdio.h>
FILE *fopen(const char *filename, const char *mode);
fopen 打开由 filename 参数指定的文件,并把它与一个文件流关联起来。mode 参数指定文件的打开方式:
r/rb 只读方式
w/wb 以写方式,文件长度截断为零
a/ab 以写方式,新内容追加在文件尾
r+/rb+/r+b 以更新方式打开(读和写),如果文件不存在出错
w+/wb+/w+b 以更新方式打开,并把文件长度截断为零,如果文件不存在,新建
a+/ab+/a+b 以更新方式打开,新内容追加在文件尾
字母 b 表示该文件是一个二进制文件而非文本文件。
UNIX/Linux 将所有文件都看做二进制文件。mode 参数必须是一个人字符串。
fopen 在成功时返回一个非空的 FILE* 指针,失败时返回 NULL 值,NULL 值在头文件 stdio.h 中定义。可用的文件流数量和文件描述符一样,都是有限制的。实际的限制是由头文件 stdio.h 中定义的 FOPEN_MAN 来定义的,它的值至少是 8,在 Linux 系统中,通常是 16。
fread 函数
fread 库函数用于从一个文件流里读取数据,数据从文件流 stream 读到由 ptr 指向的数据缓冲区里。fread 和 fwrite 都是对数据记录进行操作,size 参数指定每个数据记录的长度,计数器 nitems 给出要传输的记录个数。它的返回值是成功读到数据缓冲区里的记录个数(而不是字节数),当到达文件尾时,它的返回值可能会小于 nitems,甚至可以是零。
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);
对所有向缓冲区里写数据的标准 I/O 函数来说,为数据分配空间和检查错误是程序员的责任。
fwrite 函数
返回值是成功写入的记录个数。
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nitems, FILE *stream);
不推荐把fread和fwrite用于结构化数据,部分原因在于 fwrite 写的文件在不同的计算机体系结构之间可能不具备可移植性。
fclose 函数
fclose 库函数关闭指定的文件流 stream,使所有未写出的数据都写出。因为 stdio 库会对数据进行缓冲,所以使用 fclose 是很重要的。如果程序需要确保数据已经全部写出,就应该调用 fclose 函数。
#include <stdio.h>
int fflush(FILE *stream);
fseek 函数
fseek 函数是与 lseek 系统调用对应的文件流函数。在文件流里为下一次读写操作指定位置。offset 和 whence 参数的含义和取值与 lseek 系统调用完全一样。但是 lseek 返回的是一个 off_t 数值,而 fseek 返回一个整数。
#include <stdio.h>
int fseek(FILE *stream, long int offset, int whence);
fgetc、getc、getchar 函数
fgetc 函数从文件流里取出下一个字节并把它作为一个字符返回。当它到达文件尾或出现错误时,返回 EOF,需要通过 ferror 或 feof 来区分。
#include <stdio.h>
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar();
getc 函数的作用和 fgetc 一样,但它有可能被实现为一个宏,如果是这样,stream 参数就可能被计算不止一次,所以它不能有副作用,此外,也不能保证能够使用 getc 的地址作为一个函数指针。
getchar 函数的作用相当于 getc (stdin),它从标准输入里读取下一字符。
fputc、putc、putchar 函数
# include <stdio.h>
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);
类似于 fetc 和 getc 之间的关系,putc 函数的作用也相当于 fputc,但它可能被实现为一个宏。putchar 函数相当于 putc(c, stdout),它把单个字符写到标准输出。putchar 和 getchar 都是把字符当做 int 类型,而非 char 类型。这就允许文件尾标识符取值 -1,这是一个超出字符数字编码范围的值。
fgets、gets 函数
#include <stdio.h>
char *fgets(char *s, int n, FILE *stream);
char *gets(char *s);
fgets 把读到的字符写到 s 指向的字符串里,直到出现下面某种情况:遇到换行符、已经传输了 n-1 个字符、到达文件尾。 它会把遇到的换行符也传递到接受字符串里,再加上一个表示结尾的空字节 \0,一次调用最多只能传输 n-1 个字符,因为它必须把空字节加上以结束字符串。
当成功完成时,fgets 返回一个指向字符串 s 的指针。如果文件流已经到达文件尾巴,fgets 会设置这个文件流的 EOF 标识并返回一个空指针。如果出现读错误,fgets 返回一个空指针并设置 errno 以指出错误的类型。
gets 函数类似于 fgets,只不过它从标准输入读取数据并丢弃遇到的换行符。它在接收字符串的尾部加上一个 NULL 字节。
gets 对传输字符的个数并没有限制,所以它可能会溢出自己的传输缓冲区,应该避免使用它并用fgets来代替。
格式化输入和输出
printf、fprintf、sprintf 函数
#include <stdio.h>
int printf(const char *format, ...);
int sprintf(char *s, const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
printf 函数把自己的输出送到标准输出。fprintf 函数把自己的输出送到一个指定的文件流。sprintf 函数把自己的输出和一个结尾空字符写到作为参数传递过来的字符串 s 里。这个字符串必须足够容纳所有的输出数据。
%d, %i 以十进制格式输出一个整数
%o, %x 以八进制或十六进制格式输出一个整数
%c 输出一个字符
%s 输出一个字符串
%f 输出一个单精度浮点数
%e 以科学计数法格式输出一个双精度浮点数
%g 以通用格式输出一个双精度浮点数
| 格式 | 参数 | 输出|
|:---:|:----:|:---:|
%10s "hello" | hello|
%-10s "hello" |hello |
%10d 1234 | 1234|
%-10d 1234 |1234 |
%010d 1234 |0000001234 |
%10.4f 12.34 | 12.3400|
%*s 10,"hello" | hello|
上表中所有示例都输出到一个10个字符宽的区域里。负值表示数据在该字段里以左对齐的格式输出。可变字段宽度用一个星号来表示,在这种情况下,下一个参数用来表示字段宽。
根据POSIX规范的要求,printf不对数据字段进行截断,而是库充数据字段以适应数据的宽度。
%10s "HelloTherePeeps" |HelloTherePeeps|
scanf、fscanf、sscanf
#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *s, const char *format, ...);
scanf 函数读入的值将保存到对应的变量中,这些变量的类型必须正确,并且它们必须精确匹配格式字符串。否则,内存数据就可能会遭到破坏。
int num;
scanf("Hello %d", &num);
这个 scanf 调用只有在标准输入中接下来的五个字符匹配到"Hello"的情况下才会成功。然后,如果后面的字符构成了一个可识别的十进制数字,该数字就将被读入并赋值给变量 num。格式字符串中的空格用于忽略输入数据中位于转换控制符之间的各种空白字符(空格、制表符、换页符、换行符)。%d 将持续读取输入,忽略空格和换行符,直到找到一组数组为止。
%d, %i 读取一个十进制整数
%o, %x 以八进制或十六进制格式读入一个整数
%c 读取一个字符
%s 读取一个字符串
%f、%e、%g 读取一个单精度浮点数
%[] 读取一个字符集合
%% 读取一个%字符
%ld 表示读入一个长整数,%hd 表示读入读入一个短整数,%lg 表示读入一个双精度浮点数。
以星号 * 开头的控制符表示对应位置上的输入数据将被忽略。这意味着,这个数据不会被保存,因此不需要使用一个变量来接收。
%c 控制符从输入中读取一个字符,它不会跳过起始的空白字符。
使用 %s 控制符来扫描字符串,它会跳过起始的空白字符,并且会在字符串里出现第一个空白字符出停下来,所以最好用它来读取单词而非一般意义的字符串。或者结合使用 fgets 和 sscanf 从输入中读入一行数据,再对它进行扫描。这样可以避免可能被恶意用户利用的缓冲区溢出。
使用 %[] 控制符读取一个由一个字符集合中的字符构成的字符串。格式字符串%[A-Z]将读取一个由大写字母构成的字符串。如果字符集中的第一个字符是^,就表示将读取一个由不属于该字符集合中的字符构成的字符串。
scanf 函数的返回值是它成功读取的数据项个数,如果在读第一个数据项时失败了,它的返回值就将是零。如果在匹配第一个数据项之前就已经到达了输入的结尾,它就会返回 EOF。
scanf系列函数的问题:
- 从历史来看,它们的具体实现都有漏洞;
- 它们的使用不够灵活;
- 使用它们编写的代码不容易看出究竟在干什么;
你应该尝试其它函数,使用 fread 或 fgets 来读取输入行,再用字符串函数将输入分解为所需要的数据项。
其它流函数
- fgetpos:获得文件流的当前(读写)位置
- fsetpos:设置文件流的当前(读写)位置
- ftell:返回文件流当前(读写)位置的偏移值
- rewind:重置文件流里的读写位置
- freopen:重新使用一个文件流
- setvbuf:设置文件流的缓冲机制
- remove:相当于 unlink 函数,如果它的 path 参数是一个目录的话,其作用就相当于 rmdir 函数。
#include <stdio.h>
#include <stdlib.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);
}
jiaming@jiaming-pc:~/Documents/test$ TIMEFORMAT="" time ./a.out
0.11user 0.03system 0:00.17elapsed 84%CPU (0avgtext+0avgdata 1224maxresident)k
20488inputs+20480outputs (0major+59minor)pagefaults 0swaps
运行时间不如底层数据块复制版本。stdio库在FILE结构里使用了一个内部缓冲区,只有在缓冲区满时才进行底层系统调用。
文件流错误
为了表明错误,许多stdio.h库函数会返回一个超出范围的值,比如空指针或 EOF 常数。此时,错误由外部变量 errno 指出:
#include <errno.h>
extern int errno;
许多函数都可能改变 errno 的值。它的值只有在函数调用失败时才有意义。必须在函数表明失败之后立刻对其进行检查。总是应该将它先复制到另一个变量中。
可以通过检查文件流的状态来确定是否发生了错误,或者是否到达了文件尾。
#include <stdio.h>
int ferror(FILE *stream);
int feof(FILE *stream);
void clearerr(FILE *stream);
ferror 函数测试一个文件流的错误标识,如果该标识被设置就返回一个非零值,否则返回零。
feof 函数测试一个文件流的文件尾标识,如果该标识被设置j就返回非零值,否则返回零。
clearerr 函数的作用是清除由 stream 指向的文件流的文件尾标识和错误标识。可以使用它从文件流的错误状态中恢复。
文件流和文件描述符
每个文件流都和一个底层文件描述符相关联,你可以把底层的输入操作与高层的文件流操作混合使用,但一般来说,这并不是一个明智的做法。
#include <stdio.h>
int fileno(FILE *stream);
FILE *fdopen(int fildes, const char *mode);
文件和目录的维护
chmod 系统调用
可以通过 chmod 系统调用来改变文件或目录的访问权限。
#include <sys/stat.h>
int chmod(const char *path, mode_t mode);
path 参数指定的文件被修改为具有 mode 参数给出的访问权限。参数 mode 的定义与 open 系统调用中的一样,也是对所要求的访问权限进行按位 OR 操作。
chown 系统调用
#include <unistd.h>
int chown(const char *path, uid_t owner, git_t group);
这个调用使用用户 ID 和 组ID的数字值和一个用于限定谁可以修改文件属主的系统值。
unlink、link、symlink系统调用
可以使用 unlink 系统调用来删除一个文件。
unlink 系统调用删除一个文件的目录项并减少它的链接数。必须拥有该文件所属目录的写和执行权限。
#include <unistd.h>
int unlink(const char *path);
int link(const char *path1, const char path2);
int symlink(const char *path1, const char path2);
如果一个文件的链接数减少到零,并且没有进程打开它,这个文件就会被删除。目录项总是被立刻删除,但文件所占用的空间要等到最后一个进程关闭它之后才会被系统回收。rm 程序使用的就是这个调用。文件上其它的链接表示这个文件还有其他名字,这通常是由 ln 程序创建的。可以使用 link 系统调用在程序中创建一个文件的新链接。
link 系统调用将创建一个指向已有文件 path1 的新链接。新目录项由 path2 给出。
mkdir 和 rmdir 系统调用
可以使用 mkdir 和 rmdir 系统调用来建立和删除目录。
#include <sys/types.h>
#include <sys/stat.h>
int mkdir(const char *path, mode_t mode);
mkdir 系统调用用于创建目录,它相当于 mkdir 程序。mkdir 调用将参数 path 作为新建目录的名字。目录的权限由参数 mode 设定,也要服从 umask 的设置情况。
#include <unistd.h>
int rmdir(const char *path);
rmdir 系统调用用于删除目录,但只有在目录为空时才行。
chdir 系统调用和 getcwd 函数
程序可以像用户在文件系统里那样来浏览目录。就像是在 shell 里使用 cd 命令来切换目录一样,程序使用的是 chdir 系统调用。
#include <unistd.h>
int chdir(const char *path);
程序可以通过调用 getcwd 函数来确定自己的当前工作目录。
#include <unistd.h>
char *getcwd(char *buf, size_t size);
getcwd 函数把当前目录的名字写到给定的缓冲区 buf 里。如果目录名的长度超出了 参数 size 给出的缓冲区长度,它就返回 NULL,如果成功,它返回指针 buf。
扫描目录
与目录操作有关的函数在 dirent.h 头文件中声明。它们使用一个名为 DIR 的结构作为目录操作的基础。被称为目录流的指向这个结构的指针被用来完成各种目录操作,其使用方法与用来操作普通文件的文件流(FILE *)非常相似。目录数据项本身则在 dirent 结构中a返回,该结构也是在 dirent.h 头文件里声明的。
opendir 函数
opendir 函数的作用是打开一个目录并建立一个目录流。如果成功,它返回一个指向 DIR 结构的指针,该指针用于读取目录数据项。
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
目录流使用一个底层文件描述符来访问目录本身,所以如果打开的文件过多,opendir 可能会失败。
readdir 函数
该函数返回一个指针,该指针指向的结构里保存着目录流 dirp 中下一个目录项的有关资料。后续的readdir调用将返回后续的目录项。如果发生错误或者到达目录尾,readdir 将返回 NULL。
#include <sys/types.h>
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
如果在readdir 函数扫描目录的同时还有其他进程在该目录里创建或删除文件,readdir 将不保证能够列出该目录里的所有文件(和子目录)。
dirent 结构中包含的目录项内容包括:
- ino_t d_ino:文件的inode节点号。
- char d_name[]:文件的名字。
telldir 函数
该函数的返回值记录着一个目录流里的当前位置。
#include <sys/types.h>
#include <dirent.h>
long int telldir(DIR *drip);
seekdir 函数
该函数的作用是设置目录流dirp的目录项指针。loc的值用来设置指针的位置,它应该通过前一个telldir调用获得。
#include <sys/types.h>
#include <dirent.h>
void seekdir(DIR *dirp, long int loc);
closedir 函数
该函数关闭一个目录流并释放与之关联的资源。它在执行成功时返回 0,发生错误时返回 -1。
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
#include <unistd.h>
#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <sys/stat.h>
#include <stdlib.h>
void printdir(char *dir, int depth)
{
DIR *dp;
struct dirent *entry;
struct stat statbuf;
if((dp = opendir(dir)) == NULL)
{
fprintf(stderr, "cannot open directory: %s\n", dir);
return;
}
chdir(dir);
while((entry = readdir(dp)) != NULL)
{
lstat(entry->d_name, &statbuf);
if(S_ISDIR(statbuf.st_mode))
{
if(strcmp(".", entry->d_name) == 0 ||
strcmp("..", entry->d_name) == 0)
continue;
printf("%*s%s/\n", depth, "", entry->d_name);
printdir(entry->d_name, depth+4);
}
else
{
printf("%*s%s\n", depth, "", entry->d_name);
}
}
chdir("..");
closedir(dp);
}
int main()
{
printf("Directory scan of /home:\n");
printdir("/home", 0);
printf("done.\n");
exit(0);
}
错误处理
许多系统调用和函数都会因为各种各样的原因而失败,在失败时,会设置外部变量 errno 的值来指明失败的原因。许多不同的函数库都把这个变量用做报告错误的标准方法。程序必须在函数报告出错之后立刻检查 errno 变量,因为它可能被下一个函数调用所覆盖,即使下一个函数自身并没有出错,也可能会覆盖这个变量。
错误代码的取值和含义都列在头文件 errno.h。
- EPERM:操作不允许。
- ENOENT:文件或目录不存在。
- EINTR:系统调用被中断。
- EIO:I/O错误。
- EBUSY:设备或资源忙。
- EEXIST:文件存在。
- EINVAL:无效参数。
- EMFILE:打开的文件过多。
- ENODEV:设备不存在。
- EISDIR:是一个目录。
- ENOTDIR:不是一个目录。
strerror 函数
该函数把错误代码映射为一个字符串,该字符串对发生的错误类型进行说明。
#include <string.h>
char *strerror(int errnum);
perror 函数
该函数也把errno变量中报告的当前错误映射到一个字符串,并把它输出到标准错误输出流。该字符串的前面先加上字符串 s 中给出的信息,再加上一个冒号和一个空格。
#include <stdio.h>
void perror(const char *s);
/proc 文件系统
Linux 将一切事物都看作文件,硬件设备在文件系统中也有相应的条目。我们使用底层系统调用这样一种特殊方式通过 /dev 目录中的文件来访问硬件。
控制硬件的软件驱动程序通常可以以某种特定的方式配置,或者能够报告相关信息。例如,一个硬盘控制程序可以被配置为使用一个特殊的DMA模式。一块网卡可以报告它是否协商了一个高速、双工的连接。
近年来,倾向于提供一种更加一致的方式来访问驱动程序的信息。这种一致的方式甚至延伸到包括与Linux内核的各种元素的通信。
Linux 提供了一个特殊的文件系统procfs,它通常以 /proc 目录的形式呈现。该目录中包含了许多特殊文件用来对驱动程序和内核信息进行更高层的访问。只要应用程序有正确的访问权限,它们就可以通过读写这些文件来获得信息或设置参数。
大多数情况下,只需要直接读取这些文件就可以获得状态信息,比如 cat /proc/cpuinfo 。
类似地,/proc/meminfo 和 /proc/version 分别给出内存使用情况和内核版本信息。
每次读取这些文件的内容时,它们所提供的信息都会及时更新。
可以通过 /proc/net/sockstat 文件获得网络套接字的使用统计:
jiaming@jiaming-pc:~$ cat /proc/net/sockstat
sockets: used 1260
TCP: inuse 8 orphan 0 tw 0 alloc 11 mem 1
UDP: inuse 7 mem 5
UDPLITE: inuse 0
RAW: inuse 0
FRAG: inuse 0 memory 0
/proc 目录中的有些条目不仅可以被读取,而且可以被修改。系统中所有运行的程序同时能打开的文件总数是 Linux 内核的一个参数。它的值可以通过读取 /proc/sys/fs/file-max 文件得到。
如果要将系统范围的文件句柄限制增加,只需要新的上限值写入 file-max 文件即可,echo 8000 > /proc/sys/fs/file-max 。
/proc 目录中以数字命名的子目录用于提供正在运行的程序的信息。
fcntl 和 mmap
fcntl 系统调用
该系统调用对底层文件描述符提供了更多的操纵方法。
#include <fcntl.h>
int fcntl(int fildes, int cmd);
int fcntl(int fildes, int cmd, long arg);
利用 fcntl 系统调用,你可以对打开的文件描述符执行各种操作,包括对它们进行复制、获取和设置文件描述符标志、获取和设置文件状态标志,以及管理建议性文件锁等。
对不同操作的选择是通过选取命令参数 cmd 不同的值来实现的,其取值定义在头文件 fcntl.h 中。根据所选择命令的不同,系统调用可能还需要第三个参数 arg。
- fcntl(fildes, F_DUPFD, newfd): 这个调用返回一个新的文件描述符,其数值等于或大于整数newfd。新文件描述符是系统描述符 fildes 的一个副本。根据已打开文件数目和 newfd 值的情况,它的效果可能和系统d调用 dup(fildes) 完全一样。
- fcntl(fildes, F_GETFD): 这个调用返回在 fcntl.h 头文件里定义的文件描述符标志,其中包括 FD_CLOEXEC,它的作用是决定是否在成功调用了某个 exec 系列的系统调用之后关闭该文件描述符。
- fcntl(fildes, F_SETFD, flags): 这个调用用于设置文件描述符标志,通常仅用来设置 FD_CLOEXEC。
- fcntl(fildes, F_GETFL)和fcntl(fildes, F_SETFL, flags):这两个调用分别用来获取和设置文件状态和访问标志。你可以利用在 fcntl.h 头文件中定义的掩码 O_ACCMODE 来提取出文件的访问模式。其他标志包括哪些当 open 调用使用 O_CREAT 打开文件时作为第三参数出现的标志。
mmap 函数
mmap(内存映射)函数的作用是建立一段可以被两个或更多程序读写的内存,还可以用在文件的处理上,可以使用某个磁盘文件的全部内容看起来就像是内存中的一个数组。如果文件由记录组成,而这些记录又能够用 C 语言中的结构来描述的话,你就可以通过访问结构数组来更新文件的内容了。
这要通过使用带特殊权限集的虚拟内存段来实现。对这些虚拟内存段的读写会使操作系统去读写磁盘文件中与之对应的部分。
mmap函数创建一个指向一段内存区域的指针,该内存区域与可以通过一个打开的文件描述符访问的文件的内容相关联。
#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off);
- off 参数来改变经共享内存段访问的文件中数据的起始偏移值。
- 打开的文件描述符由 fildes 参数给出。
- 可以访问的数据量(即内存段的长度)由len参数设置。
- addr参数来请求使用某个特定的内存地址。如果它的取值是零,结果指针就将自动分配,这是推荐的做法。
- prot 参数用于设置内存段的访问权限,是以下常数值的按位 OR 结果:
- PROT_READ 读该内存段
- PROT_WRITE 写该内存段
- PROT_EXEC 执行该内存段
- PROT_NONE 该内存段不能被访问
- flags 参数控制程序对该内存段的改变所造成的影响。
- MAP_PRIVATE 内存段是私有的,对它的修改只对本进程有效
- MAP_SHARED 把对该内存段的修改保存到磁盘文件中
- MAP_FIXED 把内存段必须位于addr指定的地址处
msync 函数的作用是:把在该内存段的某个部分或整段中的修改写回到被映射的文件中(或从被映射文件里读出)。
#include <sys/mman.h>
int msync(void *addr, size_t len, int flags);
内存段需要修改的部分由作为参数传递过来的起始地址 addr 和长度 len 确定。flags 参数控制着执行修改的具体方式。
- MS_ASYNC 采用异步写方式
- MS_SYNC 采用同步写方式
- MS_INVALIDATE 从文件中读回数据
munmap 函数的作用是释放内存段:
#include <sys/mman.h>
int munmap(void *addr, size_t len);
下面的程序演示了如何利用 mmap 和数组方式的存取操作来修改一个结构化数据文件。
#include <unistd.h>
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdlib.h>
#define NRECORDS (100)
typedef struct
{
int integer;
char string[24];
} RECORD;
int main()
{
RECORD record, *mapped;
int i, f;
FILE *fp;
fp = fopen("records.dat", "w+");
for(i = 0; i < NRECORDS; i++)
{
record.integer = i;
sprintf(record.string, "RECORD-%d", i);
fwrite(&record, sizeof(record), 1, fp);
}
fclose(fp);
fp = fopen("records.dat", "r+");
fseek(fp, 43*sizeof(record), SEEK_SET);
fread(&record, sizeof(record), 1, fp);
record.integer = 143;
sprintf(record.string, "RECORD-%d", record.integer);
fseek(fp, 43*sizeof(record), SEEK_SET);
fwrite(&record, sizeof(record), 1, fp);
fclose(fp);
f = open("records.dat", O_RDWR);
mapped = (RECORD *)mmap(0, NRECORDS*sizeof(record), PROT_READ | PROT_WRITE, MAP_SHARED, f, 0);
mapped[43].integer = 243;
sprintf(mapped[43].string, "RECORD-%d", mapped[43].integer);
msync((void*)mapped, NRECORDS*sizeof(record), MS_ASYNC);
munmap((void*)mapped, NRECORDS*sizeof(record));
close(f);
exit(0);
}
|