计算机 / 读书笔记 · 2021年12月19日 0

APIUE读书笔记chapter3

文件描述符

内核通过文件描述符来引用打开的文件。
每个进程最多能同时打开OPEN_MAX个文件描述符。
一般前3个是:STDIN_FILENO(0), STDOUT_FILENO(1), STDERR_FILENO(2),这些常量在<unistd.h>中定义。
作为对比,ISO C规定的标准库stdin,stdout,stderr是文件指针。

open和openat函数

#include <fcntl.h>

int open(const char *path, int oflag, ... /* mode_t mode */ );

int openat(int fd, const char *path, int oflag, ... /* mode_t mode */ );

只有创建文件时才会需要最后的mode参数。
因为每次open时返回的总是最小的可用文件描述符,因此可以用关闭STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,然后再打开新的文件描述符的方式重定向标准输入输出以及stderr。

openat与open的不同之处:

  1. 当openat的path是绝对路径时,fd被忽略;
  2. 当openat的path不是绝对路径时,那么就在fd所代表的目录下搜寻此path;
  3. 当openat的path是相对路径且fd为特殊值AT_FDCWD时,在当前工作目录下搜寻path;

openat解决的两个问题:

  1. 多线程情景下可用让不同线程有不同的当前工作目录;
  2. 避免time-of-check-to-time-of-use(TOCTTOU)错误。

creat函数

#include <fcntl.h>

int creat(const char *path, mode_t mode);

等价于调用open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
每次open都会建立一个新的file table entry。

close函数

#include <unistd.h>

int close(int fd);

lseek函数

#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);

whence可取值:SEEK_SET、SEEK_CUR、SEEK_END。

lseek的返回值表示操作成功后的offset,因此可用seek zero bytes的方式判断当前的文件offset。但是对于pipe,FIFO和套接字,lseek操作是不允许的。

在Intel x86处理器上的FreeBSD的/dev/kmem文件,file offset是可用为负的,所以判断lseek返回值一定和-1进行比较,而不是仅判断是否小于0。
lseek只是修改file offset,并不进行实际上的I/O操作。
file offset可以被设置得比当前文件大小大,紧接着的写操作会相应的将文件扩大(这个文件由此有了一个file hole),但是文件系统是否真正的在磁盘上开辟空间来保存这中间凭空多出的部分取决于具体的文件系统实现。

read函数

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t nbytes);

file offset的更新会早于read返回实际读取的数据。

write函数

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t nbytes);

File Sharing

Atomic Operations

  • 在打开文件时如果设置了O_APPEND标志,那么在以后的每次写操作的时候,系统都会先自动将file table entry的file offset设置为文件末尾,然后再进行写操作,且这种定位到文件末尾再写入的操作是原子性的,防止了多个进程往同一个文件里追加内容时一个进程的写入覆盖其他进程的写入问题。
  • 类似的原子操作还有检查文件是否存在,且不存在就创建该文件这种操作。这需要通过open的时候带上O_CREAT和O_EXCL选项来完成。
  • 为了避免多线程情景下多个线程共享一个file offset的困扰,可以使用pread和pwrite函数。
#include <unistd.h>

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);

dup和dup2函数

#include <unistd.h>

int dup(int fd);

int dup2(int fd, int fd2);

dup返回的总是最小可用文件描述符且会清除close-on-exec文件描述符标志位。
对于dup2:

  • 如果fd2已经被打开,那么fd2会先被关闭。然后让fd2与fd指向同一个file table entry,并且清除fd2的FD_CLOEXEC标志位。
  • 如果fd2等于fd,那么就不关闭fd2,也不会清除FD_CLOEXEC标志位。

dup2基本等效于:

close(fd2);
fcntl(fd, F_DUPFD, fd2);

但是不同之处在于dup2是一个原子操作。

sync、fsync、fdatasync函数

#include <unistd.h>

int fsync(int fd);
int fdatasync(int fd);

void sync(void);

fcntl函数

#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* int arg */ );

fcntl用于改变已经打开文件的属性。fcntl有五种用途:

  1. 复制文件描述符(cmd=F_DUPFD或者F_DUPFD_CLOEXEC)
  2. 读取/设置文件描述符标志位(cmd=F_GETFD或F_SETFD)
  3. 读取/设置文件状态标志位(cmd=F_GETFL或F_SETFL)
  4. 读取/设置异步I/O的owner(cmd=F_GETOWN或F_SETOWN)
  5. 读取/设置record locks(cmd=F_GETLK,F_SETLK,或F_SETLKW)

注意file descriptor flag指的是FD_CLOEXEC(目前实现只有这个标志位),而file status flag指的是打开文件时传入的那些O_RDONLY、O_APPEND、O_SYNC这类标志位。
另一个需要注意的地方是使用F_GETFL标志时,返回的file status flag:O_RDONLY、O_WRONLY、O_RDWR、O_EXEC、O_SEARCH这个几个flag并不是独立可以按位测试的(比如O_RDONLY、O_WRONLY、O_RDWR的值经常是0,1,2)。所以我们需要将fcntl的返回值与O_ACCMODE按位与,然后再判断和这几个值中的哪一个相等。

ioctl函数文件

操纵一些特殊文件设备时经常会用到ioctl函数,比如硬盘、socket、磁带、terminal等。记得以前写oss程序时也经常用这个函数。

/dev/fd文件

大意就是/dev/fd/n文件就代表了相应的文件描述符。

Exercises

2.要不用fcntl实现dup2。但是dup2要求两个文件描述符指向同一个file table entry,而open函数会创建新的file table entry。唯一想到的办法是利用dup函数返回当前最小可用文件描述符的特性,不停用dup函数创建新的文件描述符直到返回的文件描述符大于等于想得到的文件描述符。对于这个大于等于指定值的fd,如果它正好等于指定值,说明这个值代表的文件描述符之前是没有打开的,否则说明这个值代表的文件描述符是已经被打开过了的,那么我们就先关闭这个值对应的文件描述符,然后再dup一次,那么返回的文件描述符就是这个指定值了。最后关掉那些被我们打开但是不用的文件描述符。

5.题目省略。

测试程序:

#include <unistd.h>

int main(int argc, char **argv) {

	char hello1[] = "hello stdout";
	char hello2[] = "hello stderr";

	write(STDOUT_FILENO, hello1, sizeof(hello1));
	write(STDERR_FILENO, hello2, sizeof(hello2));

	return 0;
}

结论就是:1.>符号只更改了stdout指向的文件;2.digit1 >& digit2这种更改方式实际上的操作是让文件描述符digit2指向文件描述符digit1指向的文件(指向同一个file table entry)。

6.If you open a file for read–write with the append flag, can you still read from anywhere in the file using lseek? Can you use lseek to replace existing data in the file? Write a program to verify this.

测试程序:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char **argv) {
	char hello[] = "hello world";
	int fd = open("test.bin", O_RDWR | O_APPEND);
	if (fd < 0) {
		printf("open file failed!\n");
		return -4;
	}
	int n_write = write(fd, hello, sizeof(hello));
	if (n_write != sizeof(hello)) {
		printf("write failed!\n"
				"write %d bytes\n", n_write);
		return -1;
	}

	off_t p_seek = lseek(fd, 0, SEEK_SET);
	if (p_seek != 0) {
		printf("lseek failed!\n"
				"lseek returns %ld\n", p_seek);
		return -2;
	}

	char buf[sizeof(hello)];
	for (int i = 0; i < sizeof(hello); i++) {
		buf[i] = 0;
	}

	int n_read = read(fd, buf, sizeof(hello));
	if (n_read != sizeof(hello)) {
		printf("read after lseek failed!\n"
				"read %d bytes\n", n_read);
		return -3;
	}
	printf("read content:%s\n", buf);

	char hostile[] = "hostile!";
	p_seek = lseek(fd, 0, SEEK_SET);
	if (p_seek != 0) {
		printf("lseek failed!\n"
				"lseek returns %ld\n", p_seek);
		return -2;
	}

	n_write = write(fd, hostile, sizeof(hostile));
	if (n_write != sizeof(hostile)) {
		printf("write failed after lseek!\n"
				"write %d bytes\n", n_write);
	}


	return 0;
}

在我的Ubuntu上,lseek和read、write操作都可以成功,但是第二次write的操作是接着在hello world!后面写的,而不是在lseek重定位的文件开头。这印证了书前面所说的带O_APPEND标志打开文件时,每次写操作之前都会将相应的file offset置为文件末尾。