《UNIX环境高级编程》 文件

words: 4.8k    views:    time: 19min
I/O


Linux中所有的 I/O 设备都被抽象为文件,这样所有的输入输出都被当作对相应的文件进行读写操作。这种将设备统一映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为 Unix I/O, 使得所有的输入输出都能以一种统一的方式执行。

1. 文件描述符

对于内核而言,所有打开的文件都通过文件描述符进行引用。文件描述符是一个非负整数,当打开或创建一个文件时,内核向进程返回一个文件描述符;当读写一个文件时,也是通过文件描述符作为参数来进行系统调用。

在可移植操作系统接口规范POSIX.1(Portable Operating System Interface of UNIX)中,描述符012分别与标准输入、标准输出,标准错误关联,并作为常量定义在头文件<unistd.h>中,分别为STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO

1.1. 基本函数

1.1.1. open、openat
fcnt1.h
1
2
int open(const char *path, int oflag, ... /* mode_t mode */);
int openat(int fd, const char *path, int oflag, ... /* mode_t mode */);

若成功,返回文件描述符,出错则返回-1。参数path为要打开或创建的文件名;参数fd用来说明当path为相对路径时的起始地址;参数oflag用来说明操作的选项,用一个或多个常量进行或运算构成,至于常量则定义在fcnt1.h

O_RDONLY :只读打开
O_WRONLY :只写打开
O_RDWR :读写打开
O_EXEC :只执行打开
O_SEARCH :只搜索打开(应用于目录)
上述5个常量有且只能指定一个,下面是可选常量
O_APPEND :写入时追加到文件末尾
O_CLOEXEC :设置常量O_CLOEXEC设置为文件描述符标志
O_CREAT :若文件不存在则创建,此时需要指定新文件的访问权限mode
O_DIRECTORY :如果path引用的不是目录,则出错
O_EXCL :若同时指定O_CREAT,而文件已存在则出错,不存在则创建,因此可以用来构建原子操作
O_NOCTTY :若path引用的是终端设备,则不将该设备分配为此进程的控制终端
O_NOFOLLOW :若path引用的是符号链接,则出错
O_NONBLOCK :若path引用的FIFO、块特殊文件或字符特殊文件,则将打开后续的I/O操作设置为非阻塞方式
O_TRUNC :如果文件存在,而且为只写或读写打开,则将其长度截断为0
O_SYNC :使每次write等待物理I/O操作完成,包括由write引起的文件属性更新所需的I/O
O_DSYNC :使每次write等待物理I/O操作完成,但不需要等待文件属性的更新
O_RSYNC :使以文件描述符作为参数进行的read操作等待,直到所有对文件同一部分挂起的所有写操作全部完成

1.1.2. creat
fcnt1.h
1
int creat(const char *path, int oflag, mode_t mode);

若成功则返回文件描述符,出错则返回-1,可以等效于open(path, O_WRONLY|O_CREAT|O_TRUNC, mode);

create的不足之处是它以只写方式打开所创建的文件,如果要读取创建的文件,则在创建之后要先close,然后在open,当然也可以直接使用open(path, O_RDWR|O_CREAT|O_TRUNC, mode);

1.1.3. close
unistd.h
1
int close(int fd);

成功返回0,出错则返回-1。关闭一个文件时会释放当前进程加在该文件上的所有记录锁,而当一个进程终止时,内核也会关闭所有它打开的文件。

1.1.4. lseek
unistd.h
1
off_t lseek(int fd, off_t offset, int whence);

每个打开文件都有一个偏移量属性,用以计算距离文件开始处的字节数。通常读写操作都是从当前文件偏移量开始,并使偏移量增加所读写的字节数。当打开一个文件时,如果不指定O_APPEND属性,偏移量则默认设置为0。

若成功则返回新的文件偏移量,出错则返回-1,其中参数offset的解释与whence相关:

whence = SEEK_SET :将文件的偏移量设置为距文件开始处offset个字节
whence = SEEK_CUR :将文件的偏移量设置为当前偏移量加上offset,offset可为负
whence = SEEK_END :将文件的偏移量设置为文件长度加上offset,offset可为负

因此,通过curr_offset = lseek(fd, 0, SEEK_CUR)可以获取当前打开文件的偏移量,还可以用来确定当前文件是否支持设置偏移量,如果描述符指向的是一个管道、FIFO或网络套接字,则lseek返回-1

lseek仅将当前文件偏移量记录在内核中,并不会引起任何I/O操作,该偏移量将用于下一个读写操作。文件偏移量可以大于当前文件长度,然后在下一次写入时将加长该文件,并在文件中构成一个空洞,这是允许的,但文件中没有写过的字节都被读为0,文件空洞也并不要求在磁盘上占用存储区,具体处理方式与文件系统的实现有关。

1.1.5. read、write
unistd.h
1
2
ssize_t read(int fd, void *buf, size_t nbytes);
ssize_t write(int fd, const void *buf, size_t nbytes);

read成功则返回读到的字节数(读到文件末尾则返回0),出错则返回-1。write的返回值通常与nbytes值相同,否则表示出错。

1.2. 示例:cp

如下,通过readwrite函数复制一个文件

vim test.cp.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "apue.h"

#define BUFF_SIZE 4096

int main(void){
int n;
char buf[BUFF_SIZE];

while((n = read(STDIN_FILENO, buf, BUFF_SIZE)) > 0)
if(write(STDOUT_FILENO, buf, n) != n)
err_sys("write error");

if(n < 0)
err_sys("write error");
exit(0);
}

// gcc test.cp.c -o cp && cp test.cp.c test.cp.c.copy

大多数文件系统为改善性能都会采用某种预读技术,当检测到正在进行顺序读取时,系统会试图读入比应用程序所要求的更多数据,并假设应用很快就会读取这些数据。当然理想情况是将缓存大小设置成与磁盘块长度一致,比如在Linux ext4的文件系统上,可以设置为4096。

2. 共享文件

内核使用3种数据结构表示打开文件,它们的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响:

  • 每个进程在进程表中都有一个记录项,记录项中包含一个进程打开的所有文件的描述符表,每个描述符占用一项,
    每一项关联一个文件描述符标志,和一个指向文件表项的指针;

  • 内核为每个打开的文件维持一张文件表,表中包含文件状态标志(读写、同步阻塞等)、当前文件偏移量、指向该文件v节点表项的指针;

  • 每个打开的文件(或设备)都有一个v节点结构,v节点中包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点中还包含了文件的i节点(索引节点,其中包含了文件的所有者、文件长度、指向文件实际数据在磁盘位置的指针等)。Linux并没有使用v节点,而是使用的通用i节点,但在概念上是一样的。

下图展示了一个进程打开两个不同的文件,对应的3张表之间的关系。从UNIX[Thompson 1978]以来,这三张表之间的关系一直保持至今,这种关系对于在不同进程之间共享文件的方式非常重要。

如果两个独立进程各自打开同一个文件,则有下图所示关系:

对于一个给定的文件只有一个v节点表项,但对于每个打开该文件的进程都有各自的一个文件表项(以便记录各自的偏移量),另外,也可能多个文件描述符指向同一文件表项。比如fork时,父进程、子进程对于各自的每一个打开文件描述符都共享同一个文件表项。

2.1. 基本函数

2.1.1. pread、pwrite
unistd.h
1
2
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);

pread相当于先调用lseek后再调用read操作,pwrite相当于先调用lseek后再调用write操作,区别在于这里是原子操作,并且不影响当前文件偏移量。

2.1.2. dup、dup2
unistd.h
1
2
int dup(int fd);
int dup2(int fd, int fd2);

复制文件描述符,如果成功则返回新的文件描述符,出错则返回-1。dup2可以用fd2指定新描述符的值,如果fd2已打开,则先将其关闭。

2.1.3. sync、fsync、fdatasync
unistd.h
1
2
3
int sync(void);
int fsync(int fd);
int fdatasync(int fd);

一般向文件写入数据时,内核会将数据先复制到缓冲区,然后排入队列,晚些时候再写入磁盘,即延迟写。当内核复用缓冲区来存放其它数据时,它会将缓冲的数据写入磁盘,为了保证磁盘上实际数据与缓冲的内容一致,UNIX系统提供了上面几个调用来主动同步。

sync :sync只是将所有修改过的块缓冲区排入写队列,然后就返回,并不等待实际写磁盘操作结束。系统守护进程update会负责周期性(一般每隔30s)地调用sync,保证定期冲洗内核的块缓冲区
fsync :fsync只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。fsync可用于数据库这样的应用,这种应用需要确保修改过的块立即写到磁盘上
fdatasync :fdatasync类似于fsync,区别在于它只影响文件的数据部分,而不包括文件的属性
2.1.4. fcnt1
fcnt1.h
1
int fcnt1(int fd, int cmd, ... /* int arg */);

fcnt1用于改变打开文件的属性,对应有以下5种功能:

  1. 复制一个已有的描述符(cmd = F_DUPFD 或 F_DUPFD_CLOEXEC)
  2. 获取/设置文件描述符标志(cmd = F_GETFD 或 F_SETFD)
  3. 获取/设置文件状态标志(cmd = F_GETFL 或 F_SETFL)
  4. 获取/设置异步I/O所有权(cmd = F_GETOWN 或 F_SETOWN)
  5. 获取/设置记录锁(cmd = F_GETLK、F_SETLK 或 F_SETLKW)

3. 文件类型

  • 普通文件,最常用的文件类型,即包含了某种形式的数据,至于是文本数据还是二进制并无区别。但是对于可执行的二进制文件,需要遵循一种标准,以便内核能够理解其格式,以及确定程序文本和数据的加载位置;

  • 目录文件,这种文件包含了其它文件的名字以及指向与这些文件有关信息的指针。对于一个目录有读权限的任一进程都可以读取该目录的内容,但是只有内核可以直接写目录文件,进程必须调用系统函数才能更改目录;

  • 块特殊文件,提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位进行;

  • 字符特殊文件,提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符特殊文件,要么是块特殊文件(FreeBSD不再支持块特殊文件,对设备的所有访问需要通过字符特殊文件进行);

  • FIFO,用于进程间通信,有时也称为命名管道;

  • 套接字,用于进程间的网络通信,套接字也可用于在一台机子上进程之间的非网络通信;

  • 符号链接,这种类型的文件指向另一个文件;

3.1. 基本函数

3.1.1. stat、fstat、fstatat、lstat
sys/stat.h
1
2
3
4
int stat(const char *restrict pathname, struct stat *restrict buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *restrict pathname, struct stat *restrict buf);
int fstatat(int fd, const char *restrict pathname, struct stat *restrict buf, int flag);

stat函数可以返回与命名文件相关的信息结构,fstat函数获取已在描述符fd上打开文件的有关信息。lstatstat类似,但是当命名文件是一个符号链接时,其返回的是符号链接的有关信息,而不是所指向的文件信息。

fstatat函数为一个相对于当前打开目录的路径名返回的统计信息,参数flag控制着是否跟随着一个符号链接,如果设置为AT_SYMLINK_NOFOLLOW,将返回符号链接本身的信息,而默认返回的是符号链接所指向的实际文件信息。如果参数pathname是一个绝对路径,参数fd将被忽略,而如果是一个相对路径,并且fd的值为AT_FDCWD,将会计算相对于当前目录的pathname

参数buf是一个指针,它指向一个必须提供的结构,然后函数中会负责来填充信息。结构的实际定义可能随具体实现会有所不同,但其基本形式可以如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct stat{
mode_t st_mode; /* file type & mode(permissions) */
ino_t st_ino; /* i-node number(serial number) */
dev_t st_dev; /* device number(file system) */
dev_t st_rdev; /* device number for special files */
nlink_t st_nlink; /* number of links */
uid_t st_uid; /* user id of owner */
gid_t st_gid; /* group id of owner */
off_t st_size; /* size in bytes, for regular files */
struct timespec st_atime; /* time of last access */
struct timespec st_mtime; /* time of last modification */
struct timespec st_ctime; /* time of last file status change */
blksize_t st_blksize; /* best I/O block size */
blkcnt_t st_blocks; /* number of disk blocks allocated */
};

3.2. 示例:获取文件类型

vim gettype.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <stdlib.h>
#include<sys/stat.h>

int main(int argc, char *argv[]){
int i;
struct stat buf;
char *ptr;

for(i = 1; i < argc; i++){
printf("%s: ", argv[i]);
if(lstat(argv[i], &buf) < 0){
printf("lstat error");
continue;
}

if(S_ISREG(buf.st_mode))
ptr = "普通文件";
else if(S_ISDIR(buf.st_mode))
ptr = "目录文件";
else if(S_ISCHR(buf.st_mode))
ptr = "字符特殊文件";
else if(S_ISBLK(buf.st_mode))
ptr = "块特殊文件";
else if(S_ISFIFO(buf.st_mode))
ptr = "管道或FIFO";
else if(S_ISLNK(buf.st_mode))
ptr = "符号链接";
else if(S_ISSOCK(buf.st_mode))
ptr = "套接字";
else
ptr = "unknown mode";
printf("%s\n", ptr);
}
exit(0);
}
gcc gettype.c -o gettype
1
2
3
4
5
6
/etc/passwd: 普通文件
/etc: 目录文件
/dev/log: 套接字
/dev/tty: 字符特殊文件
/dev/sr0: 块特殊文件
/dev/cdrom: 符号链接

4. 文件系统

这里介绍下UNIX文件系统的基本结构,同时,了解下i节点和指向i节点的目录项之间的区别。可以将一个磁盘分成多个区,每个区包含一个文件系统

i节点是固定长度的记录项,它包含有关文件的大部分信息,每个i节点中都有一个链接计数(如下图,有两个目录项指向同一个i节点),表示指向该i节点的目录项数,只有当链接计数为0时,才可以删除文件,即释放文件占用的数据块。这就意味着解除一个文件的链接,并不等于释放文件占用的数据块,这也是为什么删除一个目录项的函数命名为unlink而不是delete的原因。

i节点包含了文件有关的所有信息,如文件类型、访问权限位、文件长度、指向文件数据块的指针等。stat结构中的大多数信息都取自i节点,只有两项重要数据存放在目录项中:文件名和i节点编号。只是一个目录项不能指向另一个文件系统的i节点,在不更换文件系统的情况下进行mv文件重命名时,实际并未发生文件移到,只需构造一个指向先有i节点的新目录项,并删除旧的目录项。

另一种链接类型为符号链接,符号链接文件在其指向的数据块中包含了链接指向的文件的名字。可以理解为硬链接与原文件名指向了同样的i节点,而软链接间接的指向了原文件名,然后再定位到原文件的i节点。两者的区别可以用如下示例体现:

1
2
3
4
5
6
touch target.txt
ln target.txt target.lnk
ln -s target.txt target.lnk.soft

#删除后,target.lnk正常,而target.lnk.soft没有内容,因为其对应的数据块被释放了
rm -f target.txt

考虑目录文件的链接计数,如果为叶子目录,则链接数为2,链接来自于命名该目录的目录项,以及该目录中的.项。如果存在子目录,则链接数至少为3,链接来自其子目录中的..项,每个子目录都会使其父目录的链接数加1。

4.1. 基本函数

  • link、linkat、unlink、unlinkat、remove

1. ls

vim test.ls.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <dirent.h>
#include "apue.h"

int main(int argc, char *argv[]){
DIR *dp;
struct dirent *dirp;

if(argc != 2)
err_sys("usage: ls dir_name");

if((dp = opendir(argv[1])) == NULL)
err_quit("can't open %s", argv[1]);

while((dirp = readdir(dp)) != NULL)
printf("%s\n", dirp->d_name);

closedir(dp);
exit(0);
}

3. fork

vim test.fork.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include "apue.h"

int var_globe = 6;
char buf[] = "a write to stdout\n";

int main(void){
int var_local = 88;
pid_t pid;

if(write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1)
err_sys("write error");

printf("before fork\n");

if((pid = fork()) < 0){
err_sys("fork error");
}else if(pid == 0){
var_globe++;
var_local++;
printf("子进程:");
}else{
sleep(2);
printf("父进程:");
}

printf("pid=%ld, parentPid=%ld, globe=%d, local=%d\n", getpid(), (long)getppid(), var_globe, var_local);
exit(0);
}
gcc test.fork.c -o fork && ./fork
1
2
3
4
a write to stdout
before fork
子进程:pid=23401, parentPid=23400, globe=7, local=89
父进程:pid=23400, parentPid=23207, globe=6, local=88
gcc test.fork.c -o fork && ./fork > tmp.txt
1
2
3
4
5
a write to stdout
before fork
子进程:pid=23465, parentPid=23464, globe=7, local=89
before fork
父进程:pid=23464, parentPid=23207, globe=6, local=88

附录:

apue.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h> /* for definition of errno */
#include <stdarg.h> /* ISO C variable aruments */

#define MAXLINE 4096

static void err_doit(int, int, const char *, va_list);

/*
* Nonfatal error related to a system call.
* Print a message and return.
*/
void err_ret(const char *fmt, ...){
va_list ap;
va_start(ap, fmt);
err_doit(1, errno, fmt, ap);
va_end(ap);
}

/*
* Fatal error related to a system call.
* Print a message and terminate.
*/
void err_sys(const char *fmt, ...){
va_list ap;
va_start(ap, fmt);
err_doit(1, errno, fmt, ap);
va_end(ap);
exit(1);
}

/*
* Fatal error unrelated to a system call.
* Error code passed as explict parameter.
* Print a message and terminate.
*/
void err_exit(int error, const char *fmt, ...){
va_list ap;
va_start(ap, fmt);
err_doit(1, error, fmt, ap);
va_end(ap);
exit(1);
}

/*
* Fatal error related to a system call.
* Print a message, dump core, and terminate.
*/
void err_dump(const char *fmt, ...){
va_list ap;
va_start(ap, fmt);
err_doit(1, errno, fmt, ap);
va_end(ap);
abort(); /* dump core and terminate */
exit(1); /* shouldn't get here */
}

/*
* Nonfatal error unrelated to a system call.
* Print a message and return.
*/
void err_msg(const char *fmt, ...){
va_list ap;
va_start(ap, fmt);
err_doit(0, 0, fmt, ap);
va_end(ap);
}

/*
* Fatal error unrelated to a system call.
* Print a message and terminate.
*/
void err_quit(const char *fmt, ...){
va_list ap;
va_start(ap, fmt);
err_doit(0, 0, fmt, ap);
va_end(ap);
exit(1);
}

/*
* Print a message and return to caller.
* Caller specifies "errnoflag".
*/
static void err_doit(int errnoflag, int error, const char *fmt, va_list ap){
char buf[MAXLINE];
vsnprintf(buf, MAXLINE, fmt, ap);
if (errnoflag)
snprintf(buf + strlen(buf), MAXLINE - strlen(buf), ": %s", strerror(error));
strcat(buf, "\n");
fflush(stdout); /* in case stdout and stderr are the same */
fputs(buf, stderr);
fflush(NULL); /* flushes all stdio output streams */
}


参考:

  1. 《UNIX环境高级编程》
  2. http://www.apuebook.com/apue3e.html