参考引用
- UNIX 环境高级编程 (第3版)
- 黑马程序员-Linux 系统编程
- 对于磁盘上的每个文件,文件系统都存储该文件所有者的用户 ID 和组 ID。存储这两个值只需 4 个字节 (假定每个都以双字节的整型值存放)。在检验权限期间,比较字符串较之比较整型数更消耗时间
- 但是对于用户而言,使用名字比使用数值方便,所以口令文件包含了登录名和用户 ID 之间的映射关系,而组文件则包含了组名和组 D 之间的映射关系
信号 (signal) 用于通知进程发生了某种情况。例如,若某一进程执行除法操作,其除数为 0,则将名为 SIGEPE (浮点异常) 的信号发送给该进程。进程有以下 3 种处理信号的方式
很多情况都会产生信号。终端键盘上有两种产生信号的方法
什么是系统调用?
通用库函数可能会调用一个或多个内核的系统调用,但是它们并不是内核的入口点
系统调用和库函数都以 C 函数的形式出现,两者都为应用程序提供服务
C 标准库函数和系统函数/调用关系:一个 “hello” 如何打印到屏幕的案例
头文件
一些常用的基本系统数据类型
本章描述的函数经常被称为不带缓冲的 I/O (unbuffered I/O,与标准 I/O 函数相对照)
- 不带缓冲指的是每个 read 和 write 都调用内核中的一个系统调用
- 这些不带缓冲的 I/O 函数不是 ISO C 的组成部分,但它们是 POSIX1 的组成部分
对内核而言,所有打开的文件都通过文件描述符引用
按照惯例,UNIX 系统 shell 把
在符合 POSIX.1 的应用程序中,幻数 0、1、2 虽然已被标准化,但应当把它们替换成符号常量 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 以提高可读性。这些常量都在头文件
文件描述符是指向一个文件结构体的指针
PCB 进程控制块:本质是结构体,成员是文件描述符表
#include
#include
#include // 定义 flags 参数
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode); // 仅当创建新文件时才使用第三个参数,表明文件权限
int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
#include
int close(int fd);
函数返回值
关闭一个文件时还会释放该进程加在该文件上的所有记录锁
当一个进程终止时,内核自动关闭它所有的打开文件。很多程序都利用了这一功能而不显式地用 close 关闭打开文件
#include
#include
#include
int creat(const char *pathname, mode_t mode);
函数返回值
此函数等效于
open(path, O_WRONLY | O_CREAT | O_TRUNC, mode)
creat 的一个不足之处是它以只写方式打开所创建的文件。在提供 open 的新版本之前,如果要创建一个临时文件,并要先写该文件,然后又读该文件,则必须先调用 creat、close,然后再调用 open。现在则可用上述方式调用 open 实现
// open.c
#include
#include
#include
int main(int argc, char *argv[]) {
int fd;
fd = open("./AUTHORS.txt", O_RDONLY);
printf("fd = %d\n", fd);
close(fd);
return 0;
}
$ gcc open.c -o open
$ ./open
# 输出如下,表示文件存在并正确打开
fd = 3
// open2.c
#include
#include
#include
int main(int argc, char *argv[]) {
int fd;
fd = open("./AUTHORS.cp", O_RDONLY | O_CREAT, 0644); // rw-r--r--
printf("fd = %d\n", fd);
close(fd);
return 0;
}
$ gcc open2.c -o open2
$ ./open2
fd = 3
$ ll
# 创建了一个新文件 AUTHORS.cp,且文件权限对应于 0644
-rw-r--r-- 1 yue yue 0 9月 10 22:19 AUTHORS.cp
// open3.c
#include
#include
#include
int main(int argc, char *argv[]) {
int fd;
// 如果文件存在,以只读方式打开并且截断为 0
// 如果文件不存在,则把这个文件创建出来并指定权限为 0644
fd = open("./AUTHORS.cp", O_RDONLY | O_CREAT | O_TRUNC, 0644); // rw-r--r--
printf("fd = %d\n", fd);
close(fd);
return 0;
}
$ gcc open3.c -o open3
$ ./open3
# 输出如下,表示文件存在并正确打开
fd = 3
$ ll
# 首先在 AUTHORS.cp 文件中输入内容,然后经过 O_TRUNC 截断后为 0
-rw-r--r-- 1 yue yue 0 9月 10 22:19 AUTHORS.cp
$ umask
0002 # 表明默认创建文件权限为 ~umask = 775(第一个 0 表示八进制)
// open4.c
#include
#include
#include
int main(int argc, char *argv[]) {
int fd;
fd = open("./AUTHORS.cp2", O_RDONLY | O_CREAT | O_TRUNC, 0777); // rwxrwxrwx
printf("fd = %d\n", fd);
close(fd);
return 0;
}
$ gcc open4.c -o open4
$ ./open4
fd = 3
$ ll
# 创建了一个新文件 AUTHORS.cp2,且文件权限为 mode & ~umask = 775(rwxrwxr-x)
-rwxrwxr-x 1 yue yue 0 9月 10 22:38 AUTHORS.cp2*
// open5.c
#include
#include
#include
#include
#include
int main(int argc, char *argv[]) {
int fd;
fd = open("./AUTHORS.cp4", O_RDONLY);
printf("fd = %d, errno = %d : %s\n", fd, errno, strerror(errno));
close(fd);
return 0;
}
$ gcc open5.c -o open5
$ ./open5
fd = -1, errno = 2 : No such file or directory
// open6.c
#include
#include
#include
#include
#include
int main(int argc, char *argv[]) {
int fd;
fd = open("./AUTHORS.cp3", O_WRONLY); // AUTHORS.cp3 文件权限为只读
printf("fd = %d, errno = %d : %s\n", fd, errno, strerror(errno));
close(fd);
return 0;
}
$ gcc open6.c -o open6
$ ./open6
fd = -1, errno = 13 : Permission denied
$ mkdir mydir # 首先创建一个目录
// open7.c
#include
#include
#include
#include
#include
int main(int argc, char *argv[]) {
int fd;
fd = open("mydir", O_WRONLY);
printf("fd = %d, errno = %d : %s\n", fd, errno, strerror(errno));
close(fd);
return 0;
}
$ gcc open7.c -o open7
$ ./open7
fd = -1, errno = 21 : Is a directory
#include
#include
off_t lseek(int fd, off_t offset, int whence);
每个打开文件都有一个与其相关联的 “当前文件偏移量”,通常是一个非负数,用以度量从文件开始处计算的字节数
lseek 中的 l 表示长整型
函数返回值
按系统默认的情况,当打开一个文件时,除非指定 O_APPEND 选项,否则该偏移量被设置为 0
对参数 offset 的解释与参数 whence 的值有关
lseek 仅将当前的文件偏移量记录在内核中,它并不引起任何 I/O 操作。然后,该偏移量用于下一个读或写操作
文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为 0
#include
#include
#include
#include
#include
int main(void) {
int fd, n;
char msg[] = "It's a test for lseek\n";
char ch;
fd = open("lseek.txt", O_RDWR | O_CREAT, 0644);
if (fd < 0) {
perror("open lseek.txt error");
exit(1);
}
// 使用 fd 对打开的文件进行写操作,读写位置位于文件结尾处
write(fd, msg, strlen(msg));
// 若注释下行代码,由于文件写完之后未关闭,读、写指针在文件末尾,所以不调节指针,直接读取不到内容
lseek(fd, 0, SEEK_SET); // 修改文件读写指针位置,位于文件开头
while ((n = read(fd, &ch, 1))) {
if (n < 0) {
perror("read error");
exit(1);
}
write(STDOUT_FILENO, &ch, n); // 将文件内容按字节读出,写出到屏幕
}
close(fd);
return 0;
}
// lseek_size.c
#include
#include
#include
#include
#include
int main(int argc, char *argv[]) {
int fd = open(argv[1], O_RDWR);
if (fd == -1) {
perror("open error");
exit(1);
}
int length = lseek(fd, 0, SEEK_END);
printf("file size: %d\n", length);
close(fd);
return 0;
}
$ gcc lseek_size.c -o lseek_size
$ ./lseek_size fcntl.c # fcntl.c 文件大小为 678
678
// 修改案例 2 中下行代码(扩展 111 大小)
// 这样并不能真正扩展,使用 cat 命令查看文件大小未变化
int length = lseek(fd, 111, SEEK_END);
// 在 printf 函数下行写如下代码(引起 IO 操作)
write(fd, "\0", 1); // 结果便是在扩展的文件尾部追加文件空洞
#include
#include
#include
#include
#include
int main(int argc, char*argv[]) {
int ret = truncate("dict.cp", 250);
printf("ret = %d\n", ret);
return 0;
}
lseek 读取的文件大小总是相对文件头部而言。用 lseek 读取文件大小实际用的是读写指针初、末位置的偏移差,一个新开文件,读、写指针初位置都在文件开头。如果用这个来扩展文件大小,必须引起 IO 才行,于是就至少要写入一个字符
#include
// ssize_t 表示带符号整型;void* 表示通用指针
// 参数1:文件描述符;参数2:存数据的缓冲区;参数3:缓冲区大小
ssize_t read(int fd, void *buf, size_t count);
#include
// 参数1:文件描述符;参数2:待写出数据的缓冲区;参数3:数据大小
ssize_t write(int fd, const void *buf, size_t count);
函数返回值
write 出错的一个常见原因是:磁盘已写满,或者超过了一个给定进程的文件长度限制
对于普通文件,写操作从文件的当前偏移量处开始。如果在打开该文件时,指定了 O_APPEND 选项,则在每次写操作之前,将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数
阻塞 (Block):当进程调用一个阻塞的系统函数时,该进程被置于睡眠 (Sleep) 状态,这时内核调度其它进程运行,直到该进程等待的事件发生了 (比如网络上接收到数据包,或者调用 sleep 指定的睡眠时间到了) 它才有可能继续运行。与睡眠状态相对的是运行 (Running) 状态,在 Linux 内核中,处于运行状态的进程分为两种情况
读常规文件是不会阻塞的,不管读多少字节,read 一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用 read 读终端设备就会阻塞,如果网络上没有接收到数据包,调用 read 从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定
// block_readtty.c
#include
#include
#include
int main(void) {
char buf[10];
int n;
n = read(STDIN_FILENO, buf, 10);
if (n < 0){
perror("read STDIN_FILENO");
exit(1);
}
write(STDOUT_FILENO, buf, n);
return 0;
}
$ gcc block_readtty.c -o block
$ ./block # 此时程序在阻塞等待输入,下面输入 hello 后回车即结束
hello
hello
// nonblock_readtty.c
#include
#include
#include
#include
#include
#include
#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "time out\n"
int main(void) {
char buf[10];
int fd, n, i;
// 设置 /dev/tty 非阻塞状态(默认为阻塞状态)
fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);
if(fd < 0) {
perror("open /dev/tty");
exit(1);
}
printf("open /dev/tty ok... %d\n", fd);
for (i = 0; i < 5; i++) {
n = read(fd, buf, 10);
if (n > 0) { // 说明读到了东西
break;
}
if (errno != EAGAIN) {
perror("read /dev/tty");
exit(1);
} else {
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
sleep(2);
}
}
if (i == 5) {
write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
} else {
write(STDOUT_FILENO, buf, n);
}
close(fd);
return 0;
}
$ gcc block_readtty.c -o block
$ ./block # 此时程序在阻塞等待输入,下面输入 hello 后回车即结束
hello
hello
// 将一个文件的内容复制到另一个文件中:通过打开两个文件,循环读取第一个文件的内容并写入到第二个文件中
#include
#include
#include
#include
int main(int argc, char* argv[]) {
char buf[1]; // 定义一个大小为 1 的字符数组,用于存储读取或写入的数据
int n = 0;
// 打开第一个参数所表示的文件,以只读方式打开
int fd1 = open(argv[1], O_RDONLY);
if (fd1 == -1) {
perror("open argv1 error");
exit(1);
}
// 打开第二个参数所表示的文件,以可读写方式打开,如果文件不存在则创建,如果文件存在则将其清空
int fd2 = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0664);
if (fd2 == -1) {
perror("open argv2 error");
exit(1);
}
// 循环读取第一个文件的内容,每次最多读取 1024 字节
// 将返回的实际读取字节数赋值给变量 n
while ((n = read(fd1, buf, 1024)) != 0) {
if (n < 0) {
perror("read error");
break;
}
// 将存储在 buf 数组中的数据写入文件描述符为 fd2 的文件
write(fd2, buf, n);
}
close(fd1);
close(fd2);
return 0;
}
// 使用了 C 标准库中的文件操作函数 fopen()、fgetc() 和 fputc() 来实现文件的读取和写入
#include
#include
#include
#include
int main(int argc, char* argv[]) {
FILE *fp, *fp_out;
int n = 0;
fp = fopen("hello.c", "r");
if (fp == NULL) {
perror("fopen error");
exit(1);
}
fp_out = fopen("hello.cp", "w");
if (fp_out == NULL) {
perror("fopen error");
exit(1);
}
// 判断是否读取到文件结束符 EOF
while ((n = fgetc(fp)) != EOF) {
fputc(n, fp_out); // 将读取的字符写入输出文件
}
fclose(fp);
fclose(fp_out);
return 0;
}
系统函数并不一定比库函数快,能使用库函数的地方就使用库函数
标准 I/O 函数自带用户缓冲区,系统调用无用户级缓冲,系统缓冲区是都有的
UNIX 系统支持在不同进程间共享打开文件
内核使用 3 种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响
打开文件的内核数据结构
文件描述符标志和文件状态标志在作用范围方面的区别:前者只用于一个进程的一个描述符,而后者则应用于指向该给定文件表项的任何进程中的所有描述符
一般而言,原子操作 (atomic operation) 指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集
考虑一个进程,它要将数据追加到一个文件尾端
if(lseek(fd, OL, 2) < 0)
err_sys("lseek error");
if(write(fd, buf, 100) != 100)
err_sys("write error");
假定有两个独立的进程 A 和 B 都对同一文件进行追加写操作,每个进程都已打开该文件但未使用 O_APPEND 标志
问题出在逻辑操作 “先定位到文件尾端,然后写”,它使用了两个分开的函数调用
- 解决方法:使这两个操作对于其他进程而言成为一个原子操作。任何要求多于一个函数调用的操作都不是原子操作,因为在两个函数调用之间,内核有可能会临时挂起进程
- UNIX 系统为这样的操作提供了一种原子操作方法,即在打开文件时设置 O_APPEND 标志。这样做使得内核在每次写操作之前,都将进程的当前偏移量设置到该文件的尾端处,于是在每次写之前就不再需要调用 lseek
#include
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
pread 函数返回值
pwrite 函数返回值
调用 pread 相当于调用 lseek 后调用 read,但是 pread 又与这种顺序调用有下列重要区别
#include
int dup(int fd);
int dup2(int fd, int fd2);
函数返回值
由 dup 返回的新文件描述符一定是当前可用文件描述符中的最小数值
对于 dup2,可以用 fd2 参数指定新描述符的值
复制一个描述符的另一种方法是使用 fcntl 函数,以下函数调用等价
dup(fd);
fcntl(fd, F_DUPFD, 0);
// 以下情况并不完全等价
// (1) dup2 是一个原子操作,而 close 和 fcnt1 包括两个函数调用
// 有可能在 close 和 fcntl 之间调用了信号捕获函数,它可能修改文件描述符
// (2) dup2 和 fcntl 有一些不同的 errno
dup2(fd, fd2);
close(fd2);
fcntl(fd, F_DUPFD, fd2);
#include
int fsync(int fd);
int fdatasync(int fd);
void sync(void);
#include
#include
// 参数 3 可以是整数或指向一个结构的指针
int fcntl(int fd, int cmd, ... /* int arg */ );
// 终端文件默认是阻塞读的,这里用 fcntl 将其更改为非阻塞读
#include
#include
#include
#include
#include
#include
#define MSG_TRY "try again\n"
int main(void) {
char buf[10];
int flags, n;
flags = fcntl(STDIN_FILENO, F_GETFL);
if (flags == -1) {
perror("fcntl error");
exit(1);
}
flags |= O_NONBLOCK; // 与或操作,打开 flags
int ret = fcntl(STDIN_FILENO, F_SETFL, flags);
if (ret == -1) {
perror("fcntl error");
exit(1);
}
tryagain:
n = read(STDIN_FILENO, buf, 10);
if (n < 0) {
if (errno != EAGAIN) {
perror("read /dev/tty");
exit(1);
}
sleep(3);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
write(STDOUT_FILENO, buf, n);
return 0;
}
#include
int ioctl(int fd, unsigned long request, ...);
#include
char* strcpy(char* dest, const char* src);
char* strcpy(char* dest, const char* src, size_t n);
传入参数:src
传出参数:dest
#include
char* strtok(char* str, const char* delim);
char* strtok_r(char* str, const char* delim, char** saveptr);