Linux 系统编程

第三章 文件I/O

3.1 标准 C 的I/O

1
2
3
4
5
char *fgets(char *s, size_t size, FILE *stream);
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
// ...

FILE类型是一个结构体

1
2
3
4
5
6
7
8
typedef struct iobuf {
int cnt; // 剩余字节数
char *ptr; // 下一个字符的位置
char *base; // 缓冲区的位置
int flag; // 文件访问模式
int fd; // 文件描述符
// ...
} FILE;

截屏2022-11-22 20.58.47

stdin stdout stderr 也是三个文件指针(流指针),在scanf printf等内部会被调用。

标准 I/O 缓存的三种类型

  • 全缓存

    要求填满整个缓冲区后才进行 I/O 操作。对于磁盘文件通常使用全缓存访问。可以使用fflush()刷缓存,程序在结束时也会自动刷缓存。

  • 行缓存

    涉及一个终端时(如标准输入输出)使用行缓存。行缓存满后自动输出或碰到换行符自动输出。程序在结束时也会自动刷缓存。

    1
    2
    3
    4
    5
    6
    #include <stdio.h>
    int main() {
    printf("hello world");
    while (1) sleep(1);
    return 0;
    }

    则此程序不会有输出。

  • 无缓存

    标准错误流stderr通常不带缓冲区,这使得错误信息能尽快显示出来。

3.2 文件描述符

open read lseek等函数都是内核提供的系统调用,它们不是 ANSI C 的组成部分,但都是 POSIX 的组成部分。

系统调用与 C 库的关系

截屏2022-11-23 10.15.13

文件操作方式

标准库函数:遵循 ISO 标准,基于流的 I/O,对文件指针进行操作

系统调用:兼容 POSIX 标准,基于文件描述符的 I/O,对文件描述符进行操作

文件描述符

  • 对于内核而言,所有打开的文件都由文件描述符(一个非负整数)引用。
  • 在 POSIX 应用程序中,整数0 1 2被替换成符号常数STDIN_FILENO STDOUT_FILENO STDERR_FILENO(宏),它们都定义在头文件<unistd.h>中。
  • 文件描述符的范围是 0 - OPEN_MAX,早期 UNIX 版本的上限是 19,现在很多系统将其增至 63,Linux 是 1023。

文件描述符与文件指针的转化

  • 将文件描述符转化为文件指针 FILE *fdopen(int fd, const char *mode);
  • 将文件指针转化为文件描述符 int fileno(FILE *stream);

3.3 文件操作

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
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
/*
功能:打开或创建文件
返回值:若成功返回文件描述符,若出错返回 -1

参数
- pathname:文件路径(相对或绝对)
- flags:用来说明函数操作细节的多个选项(用按位异或连接起来)
O_RDONLY 以只读方式打开
O_WRONLY 以只写方式打开
O_RDWR 以读写方式打开
O_APPEND 以追加方式打开
O_CREAT 如果不存在,按照 mode 要求创建
O_EXCL 如果同时指定了 O_CREAT,而文件已经存在,则返回 -1
O_DIRECTORY 如果 pathname 不是目录,则返回 -1
O_TRUNC 如果文件存在,且为只读/只写并成功打开,则清空文件
O_NONBLOCK 以非阻塞方式打开
- mode:新建文件的访问权限,仅当创建新文件时才使用(八进制)
*/

int creat(const char *pathname, mode_t mode);
/*
功能:创建一个文件
返回值:若成功创建,以[只写]方式打开并返回文件描述符,若出错返回 -1

此函数等效于 open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);
*/

int close(int fd);
/*
功能:关闭一个已打开的文件
返回值:若成功返回 0,若出错返回 -1

备注:当一个进程终止时,它所有打开的的文件都由内核自动关闭

*/

ssize_t read(int fd, void *buf, size_t count);
/*
功能:从打开的文件中按字节读取数据
返回值:读到的字节数,若已到文件尾返回 0,若出错返回 -1

有多种情况可能导致实际读到的字节数小于要求读的字节数 count
- 读普通文件时,在读到要求字节数之前已经到达文件末尾
- 从终端设备读时,通常一次最多读一行
- 从网络读时,网络中的缓冲机构可能限制一次读到的字节数
- 某些面向记录的设备(如磁带等)一次最多返回一个记录
- 进程由于信号中断

读操作会从文件当前的偏移量处开始,在成功返回之前,该偏移量会增加实际读到的字节数

*/

ssize_t write(int fd, void *buf, size_t count);
/*
功能:向打开的文件中按字节写入数据
返回值:写入的字节数,若出错返回 -1

对于普通文件,写操作从文件当前偏移量处开始。若打开时指定了 O_APPEND,则每次写操
作前会将偏移量设置到结尾处
*/

off_t lseek(int fd, off_t offset, int whence);
/*
功能:定位一个已打开的文件
返回值:若成功返回当前的绝对偏移量,若出错返回 -1

参数
- offset 是长整型,调用时应在数字后面加上 L
- whence 表示定位的基准点
SEEK_SET 取文件开头为基准点,此时 offset 必须非负
SEEK_CUR 取当前文件偏移量为基准点,offset 可正可负
SEEK_END 取文件末尾为基准点,offset 可正可负

备注:若取 SEEK_END 为基准点且 offset 为正并写入数据,此时文件中将出现“空洞”
备注:lseek 也可以用来确定所涉及文件是否可以设置偏移量。如果文件描述符引用了一
个管道或 FIFO,则 lseek 会返回 -1,并将 errno 设置为 EPIPE

*/

举例应用:文件拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define BUFFER_LEN 1024
void copy(int fdin, int fdout)
{
char buffer[BUFFER_LEN];
ssize_t nread;
while ((nread = read(fdin, buffer, BUFFER_LEN)) > 0) {
if (write(fdout, buffer, nread) != nread) {
fprintf(stderr, "write error: %s\n", strerror(errno));
/*
- errno 是一个系统维护的错误类型变量
- strerror 可以解析错误信息并转化为字符串输出
- 需要 #include <errno.h>、#include <string.h>
此语句等价于 perror("write error");
// perror 会在其内部调用 strerror,结合提示字符串输出
*/
exit(1);
}
}
if (nread < 0) {
fprintf(stderr, "read error: %s\n", strerror(errno));
// 等价于 perror("read error");
exit(1)
}
}

3.4 文件在内核中的数据结构

  • 文件描述符表

    • 文件描述符标志
    • 文件表项指针

    文件描述符就是这个结构体的数组下标

  • 文件表项

    • 文件状态标志
    • 文件偏移量
    • i 节点表项指针
    • 引用计数器
  • i 节点

    • 文件类型
    • 对该文件操作的函数指针
    • 文件长度
    • 文件所有者
    • 文件所在的设备
    • 文件访问权限
    • 指向存储文件的磁盘块的指针

截屏2022-11-23 16.26.33

3.5 重定向

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
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
/*
功能:复制文件描述符
返回值:若成功返回新的文件描述符,若失败返回 -1

参数
- oldfd 原先的文件描述符
- newfd 新的文件描述符

- 由 dup 返回的文件描述符一定是当前可用文件描述符中最小的那个
- 用 dup2 可以指定新描述符的数值。若 newfd 已经打开,则先将其关闭;若 oldfd
等于 newfd,则 dup2 直接返回 newfd,而不关闭它

举例应用
[1] int fd = dup(STDOUT_FILENO);
// fd 也指向了标准输出

[2] int fd = open("a.txt", O_WRONLY);
dup2(fd, STDOUT_FILENO);
// 把 fd 的文件表项指针复制给了 STDOUT_FILENO 的文件表项指针
// 实际上就是把标准输出重定向到 a.txt

*/

3.6 文件状态标志操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
/*
功能:改变已打开文件的性质
返回值:若成功返回依赖于 cmd 的非负整数,若失败返回 -1

参数:
- cmd F_GETFL 和 F_SETFL 分别表示获得/设置文件状态标志
- 若指定了 F_SETFL,则需要第三个参数指定新的文件标志状态

可以改变的状态标志是 O_APPEND、O_NONBLOCK、O_SYNC、O_ASYNC
(O_RDONLY、O_WRONLY、O_RDWR 不适用)
*/

举例应用

1
2
3
4
5
6
7
8
void add_fl(int fd, int flag)
{
int val = fcntl(fd, F_GETFL);
val |= flag;
if (fcntl(fd, F_SETFL, val) < 0) {
perror("fcntl error");
}
}
1
2
3
4
5
6
7
8
void clr_fl(int fd, int flag)
{
int val = fcntl(fd, F_GETFL);
val &= ~flag;
if (fcntl(fd, F_SETFL, val) < 0) {
perror("fcntl error");
}
}

备注:多个进程同时写一个文件时,应指定为追加模式。

3.7 文件 I/O 的五种模型

  • 阻塞 I/O 模型

    若所调用的 I/O 函数没有完成功能,进程就会挂起,直到数据到达才会返回。如终端(scanf)、网络设备的访问。

  • 非阻塞 I/O 模型

    当请求的 I/O 操作不能完成时,则不让进程休眠,而是返回一个错误。如open read write访问。

  • I/O 多路转接模型

    如果请求的 I/O 操作阻塞,并非真正阻塞 I/O,而是让其中一个函数等待,其他函数还能运行。如select函数。

  • 信号驱动 I/O 模型

    通过一个信号处理程序,使得系统可以自动捕获特定信号,进而启动 I/O。

  • 异步 I/O 模型

    当一个描述符已经准备好,可以启动 I/O时,进程会通知内核,由内核进行后续处理。(较少见)

举例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main()
{
char buffer[4096] = {'\0'};
sleep(5);

add_fl(STDIN_FILENO, O_NONBLOCK);

ssize_t size = read(STDIN_FILENO, buffer, sizeof(buffer));
if (size < 0) {
perror("read error");
exit(1);
} else if (size == 0) {
printf("read finish\n");
} else {
if (write(STDOUT_FILENO, buffer, size) != size) {
perror("write error");
}
}

return 0;
}
1
2
3
4
5
6
7
8
/*
Case [1]
运行后什么也不做,sleep 结束后输出 read error: ...
Case [2]
运行后在 sleep 期间输入 control+D,sleep 结束后输出 read finish
Case [3]
运行后在 sleep 期间任意输入内容,sleep 结束后输出该内容
*/

第四章 文件系统

4.1 文件属性和权限操作

一、文件属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct stat {
mode_t st_mode; // 文件类型和权限
ino_t st_ino; // i 节点编号
dev_t st_dev; // 设备编号
dev_t st_rdev; // 特殊文件的设备编号
nlink_t st_nlink; // 链接数
uid_t st_uid; // 拥有者 ID
gid_t st_gid; // 组 ID
off_t st_size; // 文件大小
time_t st_atime; // 最后一次访问时间
time_t st_mtime; // 最后一次修改时间
time_t st_ctime; // 最后一次状态改变时间
blksize_t st_blksize; // I/O 操作中块的大小
blkcnt_t st_blocks; // 系统分配的磁盘块的数量
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
文件属性操作函数
*/
#include <sys/types.h>
#include <sys/stat.h>
int stat(const char *pathname, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *pathname, struct stat *buf);
/*
功能:获得与 pathname 或 fd 指定的文件属性信息,存在结构体 buf 中
返回值:若成功返回 0,若失败返回 -1

lstat 与 stat 功能类型,但是当文件是一个符号链接时,lstat 返回该符号连接的有关信息,
而不是其引用的文件的信息。
*/

Linux 中的七种文件和七种宏

文件类型
普通文件(regular file) S_ISREG()
目录文件(directory file) S_ISDIR()
块特殊文件(block special file) S_ISBLK()
字符特殊文件(character special file) S_ISCHR()
命名管道 FIFO(named pipe) S_ISFIFO()
套接字(socket) S_ISSOCK()
符号链接(symbolic link) S_ISLNK()

用于判断时,可写

1
2
3
struct stat buf;
lstat("...", &buf);
if (S_ISREG(buf.st_mode)) {...}

二、权限操作

9 种文件访问权限位

对象 权限位
用户 S_IRUSR S_IWUSR S_IXUSR S_IRWXU
用户同组 S_IRGRP S_IWGRP S_IXGRP S_IRWXG
其他人 S_IROTH S_IWOTH S_IXOTH S_IRWXO

文件权限通过按位或方式构造。

1
2
3
4
int fd = open("a.txt", O_WRONLY | O_CREAT, 0652);
// 等价于
int fd = open("a.txt", O_WRONLY | O_CREAT,
S_IRUSR | S_IWUSR | S_IRGRP | S_IXGRP | S_IWOTH);

判断文件有无权限(针对当前进程)

1
2
3
4
5
6
7
8
9
#include <unistd.h>
int access(const char *pathname, int mode);
/*
功能:检查是否可以对指定文件进行某种操作
返回值:若检查成功,有权限返回 1,无权限返回 0;若检查失败返回 -1

参数
- mode R_OK、W_OK、X_OK、F_OK(是否存在)
*/

4.2 Linux 文件系统

“给磁盘中的块编号,使得磁盘看起来像一个数组。”

文件系统的三个区域(磁盘内)

  • 超级块

    存放文件系统本身的结构信息

  • i 节点表

    存放 i 节点信息列表(和内核中的 i 节点是同步对应的)

  • 数据区

    存放文件内容

从文件名到文件内容

  • 目录项中查看文件名,找到对应的 i 节点编号
  • 通过 i 节点编号找到 i 节点表中对应 i 节点
  • 根据 i 节点中数据块编号访问对应数据块,获得文件内容

截屏2022-12-03 16.41.14

4.3 硬链接和软链接

一、创建

1
2
ln hello.txt h_hello		# 创建硬链接
ln -s hello.txt s_hello # 创建软链接

二、本质

截屏2022-12-03 16.44.54

硬链接

  • 和源文件共用一个 i 节点
  • 每创建一个硬链接,链接数 +1。初始时链接数为 1
  • 文件被删除当且仅当硬链接数为 0 且无其他进程打开该文件
  • 源文件被删除后,硬链接仍有效(数据块并未删除)

软链接

  • 自己具有一个 i 节点
  • i 节点所指的数据块存放的是源文件的路径
  • 源文件被删除后,软链接失效

硬链接和软链接都是文件。每一个链接都和其他文件一样具有一个目录项。

三、系统调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <unistd.h>

int link(const char *existingpath, const char *newpath);
// 创建硬链接,失败返回 -1
int unlink(const char *pathname);
// 删除硬链接,失败返回 -1
int symlink(const char *actualpath, const char *sympath);
// 创建软链接,失败返回 -1
int readlink(const char *restrict pathname, char *restrict buf, size_t bufsize);
// 读取软链接【本身的内容】

int remove(const char *pathname)
// 删除文件或目录,失败返回 -1
int rename(const char *oldname, const char *newname);
// 文件或目录重命名

备注

  • 直接用open打开链接文件,实际打开的都是其引用的那个文件

  • 必须针对文件创建硬链接,只有超级用户才能对目录硬链接

  • 硬链接的创建必须在同一个分区
  • 创建软链接并不要求actualpath存在,此外软链接可以跨文件系统创建