原子操作和竞争条件
原子操作:将某次系统调用所要完成的各个动作作为不可中断的操作,一次性加以执行。
所有系统调用都是以原子操作的方式执行【内核保证了某系统调用中的所有步骤会作为独立操作而一次性执行,其间不会未其他进程或线程中断】
以独占方式创建一个文件
当同时指定 O_EXCL 与 O_CREAT 作为 open() 的标志位时,如果要打开的文件已存在,则 open() 将返回错误。【保证进程是文件的创建者】
试图以独占方式打开文件的错误代码
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
|
fd = open(argv[1], O_WRONLY);
if (fd != -1)
{
printf("[PID %ld] File \"%s\" already exists\n",
(long)getpid(), argv[1]);
close(fd);
}
else
{
if (errno != ENOENT)
{
errExit("open");
}
else
{
printf("[PID %ld] File \"%s\" doesn't exist yet\n",
(long)getpid(), argv[1]);
if (argc > 2)
{ /* Delay between check and create */
sleep(5); /* Suspend execution for 5 seconds */
printf("[PID %ld] Done sleeping\n", (long)getpid());
}
fd = open(argv[1], O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1)
errExit("open");
printf("[PID %ld] Created file \"%s\" exclusively\n",
(long)getpid(), argv[1]);
}
}
|
当第一次调用 open 时,希望打开的文件还不存在,而第二次调用 open 时,其他进程已经创建了该文件。此时,进程 A 将得出错误的结论,目标文件是由自己创建的。
结合 O_CREAT 和 O_EXCL 标识来一次性调用 open 可以防止这种情况。
向文件尾部追加数据
多个进程同时向一个文件尾部添加数据。
1
2
3
4
5
6
|
if (lseek(fd, 0, SEEK_END) != -1) {
errExit("lseek");
}
if (write(fd, buf, len) != len) {
fatal("Partial/failed write");
}
|
如果第一个进程执行到 lseek 和 write 之间,被执行相同代码的第二个进程所中断,那么这两个进程会在写入数据前,将文件偏移量设置为同一位置,而当第一个进程再次获得调度时,会覆盖第二个进程已写入的数据。
在打开文件时加入 O_APPEND 标识可以保证将文件偏移量的移动与数据写操作纳入同一原子操作。
文件控制操作:fcntl
fcntl() 系统调用对一个打开的文件描述符执行一系列控制操作。
1
2
3
|
#include <fcntl.h>
int fcntl(int fd, int cmd, ...); // return on success depend on cmd, or -1 on error
|
打开文件的状态标识
fnctl 可以针对一个打开的文件,获取或修改其访问模式和状态标志。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
int main(int argc, char *argv[])
{
int flags, accessMode;
char *filename = "hello.txt";
int fd;
fd = open(filename, O_RDONLY | O_SYNC);
flags = fcntl(fd, F_GETFL);
if (flags == -1)
{
exit(EXIT_FAILURE);
}
printf("%d\n", flags);
if (flags & O_SYNC)
{
printf("writes are sync\n");
}
accessMode = flags & O_ACCMODE;
if (accessMode == O_WRONLY || accessMode == O_RDWR)
{
printf("file is writable");
}
exit(EXIT_SUCCESS);
}
|
判断文件的访问模式需要使用掩码 O_ACCMODE 与 flags 相与,再将结果与三个常量进行对比,因为 O_RDONLY(0),O_WRONLY(1)和 O_RDWR(2)这三个常量不予打开文件状态标志的单个比特位对应。
可以使用 fcntl 的 F_SETFL 命令来修改打开文件的某些状态标志【允许更改的标识:O_APPEND, O_NONBLOCK, O_NOATIME, O_ASYNC 和 O_DIRECT】
1
2
3
4
5
6
7
8
9
10
|
int flags;
flags = fcntl(fd, F_GETFL);
if (flags == -1){
errExit("fcntl");
}
flags |= O_APPEND;
if (fcntl(fd, F_SETFL, flags) == -1) {
errExit("fcntl");
}
|
文件描述符和打开文件之间的关系
内核维护的三个数据结构
- 进程级的文件描述符
- 系统级的打开文件表
- 文件系统的 i-node 表
针对每个进程,内核为其维护打开的文件描述符表[open file descriptor],相关信息如下:
- 控制文件描述符的一组标志【close-on-exec】
- 打开文件句柄的引用
内核对所有打开的文件维护有一个系统级的描述表格[open file description table]【亦称为打开文件表,表中各条目称为打开文件句柄[open file handler]】。一个文件句柄存储了其打开文件相关的全部信息:
- 当前文件偏移量
- 打开文件时所用的状态标志
- 文件访问模式
- 与信号驱动 I/O 相关的设置
- 与该文件 i-node 对象的引用
每个文件系统都会为驻留其上的所有文件建立一个 i-node 表。每个文件的 i-node 信息如下:
- 文件类型和访问权限【常规文件、套接字、FIFO】
- 一个指针【指向该文件所持有的锁的列表】
- 文件的各种属性【文件大小、不同类型操作相关的时间戳】
- 两个不同的文件描述符,若指向同一打开文件句柄,将共享同一文件偏移量
- 文件描述符标识为进程和文件描述符所私有
复制文件描述符
dup() 调用复制一个打开的文件描述符 oldfd,并返回一个新的描述符,二者都指向同一打开的文件句柄。【系统会保证新描述符一定是编号值最低的未使用文件描述符】
dup2() 调用会为 oldfd 参数所指定的文件描述符创建副本,其编号由 newfd 参数指定。若 newfd 的文件描述符已经打开,dup2 首先会将其关闭。【dup2 调用会忽略 newfd 关闭期间出现的任何错误】
dup3()和 dup2 基本一致,额外新增了附加参数 flags,可以修改系统调用行为的位掩码【O_CLOEXEC】
1
2
3
4
5
6
7
8
9
|
#include <fcntl.h>
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
#define _GNU_SOURCE
int dup3(int oldfd, int newfd, int flags);
newfd = fcntl(oldfd, F_DUPFD, startfd);
|
指定文件偏移量的 I/O
系统调用 pread()和 pwrite()和 read,write 类似,只是会在 offset 参数所指定的位置进行文件 I/O 操作,而且不会改变文件的当前偏移量。
1
2
3
4
|
#include <unistd.h>
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_set offset);
|
1
2
3
4
5
6
7
|
// 模拟 pread
off_t orig;
orig = lseek(fd, 0, SEEK_CUR);
lseek(fd, offset, SEEK_SET);
s = read(fd, buf, len);
lseek(fd, orig, SEEK_SET);
|
分散输入和集中输出
readv() 和 writev() 系统调用分别实现了分散输入和集中输出的功能。【一次即可传输多个缓冲区的数据】
1
2
3
4
5
6
7
8
9
10
|
#include <sys/uio.h>
struct iovec {
void* iov_base;
size_t iov_len;
}
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
|
readv()系统调用实现了分散输入的功能:从文件描述符 fd 所指代的文件中读取一片连续的字节,然后将其散置于 iov 指定的缓冲区中。【round_robin】调用 readv() 成功将返回读取的字节数,若文件结束将返回 0。
writev() 系统调用实现了集中输出:将 iov 指定的所有缓冲区中的数据拼接起来,然后以连续的字节序列写入文件描述符 fd 指代的文件中。
当然也有指定偏移量的 preadv, pwritev。
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
|
int main(int argc, char *argv[])
{
int fd;
struct iovec iov[3];
struct stat myStruct; /* First buffer */
int x; /* Second buffer */
#define STR_SIZE 100
char str[STR_SIZE]; /* Third buffer */
ssize_t numRead, totRequired;
if (argc != 2 || strcmp(argv[1], "--help") == 0)
usageErr("%s file\n", argv[0]);
fd = open(argv[1], O_RDONLY);
if (fd == -1)
errExit("open");
totRequired = 0;
iov[0].iov_base = &myStruct;
iov[0].iov_len = sizeof(struct stat);
totRequired += iov[0].iov_len;
iov[1].iov_base = &x;
iov[1].iov_len = sizeof(x);
totRequired += iov[1].iov_len;
iov[2].iov_base = str;
iov[2].iov_len = STR_SIZE;
totRequired += iov[2].iov_len;
numRead = readv(fd, iov, 3);
if (numRead == -1)
errExit("readv");
if (numRead < totRequired)
printf("Read fewer bytes than requested\n");
/*FIXME: should use %zd here, and remove (long) cast */
printf("total bytes requested: %ld; bytes read: %ld\n",
(long)totRequired, (long)numRead);
exit(EXIT_SUCCESS);
}
|
截断文件
truncate() 和 ftruncate() 系统调用将文件大小设置为 length 参数所指定的值。
1
2
3
4
|
#include <unistd.h>
int truncate(const char* pathname, off_t length);
int ftruncate(int fd, off_t length);
|
若文件当前长度大于参数 length,调用将丢弃超出部分,若小于参数,调用将在文件尾部添加一系列空字节或是一个文件空洞。
非阻塞 I/O
在打开文件时指定 O_NONBLOCK 标识:
- 若 open 调用未能立即打开文件,则返回错误,而非陷入阻塞。【调用 open()操作 FIFO 可能会陷入阻塞】
- 调用 open 成功后,后续的 I/O 操作也是非阻塞的。【若 I/O 系统调用未能立即完成,则可能会只传输部分数据,或者系统调用失败,并返回 EAGAIN 或 EWOULDBLOCK 错误】
大文件 I/O
通常将存放文件偏移量的数据类型 off_t
实现为一个有符号的长整型。【-1 表示错误情况】
过渡型 LFS API
要使用过渡型 LFS API,必须在编译程序时定义 _LARGEFILE64_SOURCE 功能测试宏,该定义可以通过命令行指定,也可以定义源文件中包含所有头文件之前的位置。【该 API 具有处理 64 位文件大小和文件偏移量的能力:fopen64, open64, lseek64, truncate64, stat64, mmap64, setrlimit64】
调用 open64(),相当于在调用 open() 时指定 O_LARGEFILE 标识。若未指定该标识,且文件大于 2G,则会返回错误。
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
|
#define _LARGEFILE64_SOURCE
#include <sys/stat.h>
#include <fcntl.h>
#include "tlpi_hdr.h"
int main(int argc, char *argv[])
{
int fd;
off64_t off;
if (argc != 3 || strcmp(argv[1], "--help") == 0)
usageErr("%s pathname offset\n", argv[0]);
fd = open64(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1)
errExit("open64");
off = atoll(argv[2]);
if (lseek64(fd, off, SEEK_SET) == -1)
errExit("lseek64");
if (write(fd, "test", 4) == -1)
errExit("write");
exit(EXIT_SUCCESS);
}
|
/dev/fd
对于每个进程,内核都提供有一个特殊的虚拟目录 /dev/fd
。该目录包含 /dev/fd/n
形式的文件名,其中 n 时与进程中打开文件描述符相对应的编号。
1
2
3
|
// 打开 /dev/fd 目录中的一个文件等同于复制相应的文件描述符
fd = open("/dev/fd/1", O_WRONLY);
fd = dup(1);
|
创建临时文件
- 基于调用者提供的模板,mkstemp() 函数生成一个唯一的文件名并打开该文件,返回一个可用于 I/O 调用的文件描述符。
- tmpfile() 函数会创建一个名称唯一的临时文件,并以读写方式将其打开。【指定了 O_EXCL 标识,防止冲突】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
int fd;
// 模板参数采用路径名形式,其中最后6个字符必须为XXXXXX [这六个字符会被替换,以保证文件的唯一性,且修改后的]
chat template[] = "/tmp/somestringXXXXXX";
fd = mkstemp(template);
if (fd == -1) {
errExit("mkstemp");
}
printf("Generate filename was: %s\n", template);
unlink(template); // Name disappears immediately, but the file is removed only after close();
if (close(fd) == -1) {
errExit("close");
}
// tmpfile 函数签名
FILE *tmpfile(void);
|
总结
指定 O_EXCL 标识调用 open(),这确保了调用者就是文件的创建者;指定 O_APPEND 标识来调用 open(),可以确保多个进程在对同一文件追加数据时,不会覆盖彼此的输出。