UNIX环境高级编程读书笔记-三

第三章 文件I/O

本章主要讲述了5个常用函数 openreadwritelseekclose

以及在多进程中共享文件所用到的 dupfcntlsyncfsyncioctl

第三章 文件I/O

文件描述符

文件描述符 符号常量 含义
0 STDIN_FILENO 标准输入
1 STDOUT_FILENO 标准输出
2 STDERR_FILENO 标准错误

函数 open 和 openat

1
2
3
4
#include <fcntl.h> 控制文件I/O

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

open 和 openat 的区别

  1. path 是绝对路径名时, fd 参数被忽略
  2. path 参数指定的是相对路径名,fd 参数指出了相对路径名所在文件系统中的开始地址。
  3. path 参数指定的是相对路径名,fd 的参数具有特殊值 AT_FDCWD 。这时open 和 openat 函数类似。

oflag 参考链接

TODO: TOCTTOU

函数 creat

1
2
3
4
5
6
#include <fcntl.h> 控制文件I/O
int creat(const char *path, mode_t mode);
// 返回值: 若成功返回只写打开的文件描述符; 若出错,返回 -1
// 等效为
int open(path,O_WRONLY| O_CREAT | O_TRUNC, mode /*mode_t mde */);
// 如今 creat 函数不再常用,早期因为 open 中不支持 O_CREAT 参数。

操作系统中,每个进程的文件描述符不同。相互独立。

函数 close

可以用 close 函数关闭一个打开的文件。

1
2
3
# include <unistd.h>
int close(int fd);
// 返回值:成功 返回 0 出错 返回 -1

函数 lseek

lseek 可以显示地为一个文件设置偏移量

1
2
3
# include <unistd.h>
off_t lessk(int fd, off_t offset, int whence);
// 返回值:成功 返回 新的文件偏移量 出错 返回 -1

whence 参数:

  • SEEK_SET 从文件开始处偏移offset 字节
  • SEEK_CUR 当前值加 offset, offset可正可负
  • SEEK_END 从文件尾部偏移 offset 个字节, offset可正可负

一个问题:

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
➜  IO cat lseek_test.c 
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(){
int fd = creat("./test",0770);
off_t new_pos = lseek(fd, 19, SEEK_END);
write(fd, "test\n", 24);
printf("%d\n", new_pos);
new_pos = lseek(fd, 19, SEEK_CUR);
write(fd, "test\n", 24);
printf("%d\n", new_pos);
new_pos = lseek(fd, 19, SEEK_END);
write(fd, "test\n", 24);
printf("%d\n", new_pos);
return 0;
}
➜ IO gcc lseek_test.c -o lseek_test
➜ IO ./lseek_test
19
62
105
➜ IO cat test
test
%d
4test
%d
4test
%d
4% ➜ IO od -c test
0000000 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
0000020 \0 \0 \0 t e s t \n \0 % d \n \0 \0 \0 \0
0000040 001 033 003 ; 4 \0 \0 \0 005 \0 \0 \0 \0 \0 \0 \0
0000060 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 t e
0000100 s t \n \0 % d \n \0 \0 \0 \0 001 033 003 ; 4
0000120 \0 \0 \0 005 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
0000140 \0 \0 \0 \0 \0 \0 \0 \0 \0 t e s t \n \0 %
0000160 d \n \0 \0 \0 \0 001 033 003 ; 4 \0 \0 \0 005 \0
0000200 \0
0000201

➜ IO od -h test
0000000 0000 0000 0000 0000 0000 0000 0000 0000
0000020 0000 7400 7365 0a74 2500 0a64 0000 0000
0000040 1b01 3b03 0034 0000 0005 0000 0000 0000
0000060 0000 0000 0000 0000 0000 0000 0000 6574
0000100 7473 000a 6425 000a 0000 0100 031b 343b
0000120 0000 0500 0000 0000 0000 0000 0000 0000
0000140 0000 0000 0000 0000 7400 7365 0a74 2500
0000160 0a64 0000 0000 1b01 3b03 0034 0000 0005
0000200 0000
0000201

空洞的文件里面还有填充着 1b01 3b03 0034, 不知道这个是什么东西。

奇怪的 printf

函数 read

1
2
3
# include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
// 返回值:成功 返回 读到的字节数,文件尾,返回0 出错 返回 -1

函数 write

1
2
3
# include <unistd.h>
ssize_t wirte(int fd, const void *buf, size_t nbytes);
// 返回值:成功 返回已经写的字符数,返回0 出错 返回 -1

I/O 的效率

缓冲区长度 32字节的时候已经足够好了

TODO: 自己做一下测试

文件共享

UNIX 支持不同进程间共享打开文件。

原子操作

不支持原子操作实例:

早期 UNIX不支持 O_APPEND 实现 append 操作需要:

1
2
3
4
if (lseek(fd, OL, SEEK_END) < 0) /*position to EOF */
err_sys("lseek error");
if (write(fs, buf, 100) != 100) /*and write*/
err_sys("write error");

在多进程中,如果在 lseek 后发生了进程的切换,那么就有很大可能发生文件的写入覆盖错误。

函数 pread 与 pwrite

pread 与 pwrite 支持原子操作

1
2
3
#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);

PS. 不更新文件偏移量

创建文件

TODO: 坑 待填

函数 dup 和 dup2

1
2
3
4
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
// 返回值:成功 返回新的文件描述符 出错 返回 -1

实验:

1
2
3
4
5
6
7
8
9
10
11
➜  IO cat -n dup_test.c            
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main(){
5 dup2(1, 0); //自己定义文件描述符,更改标准输入输出
6 write(0,"test",100);
7 return 0;
8 }
➜ IO ./dup_test
test���dD���L=����t���������zRx����/%

函数 sync,fsync 和 fdatasync

功能: 保持文件在高速缓存与磁盘同步,数据一致性。

1
2
3
4
5
#include<unistd.h>
int fsync(int fd);
int fatasync(int fd);
// 返回值:成功 ,返回0 出错 返回 -1
void sync(void);
  • sync 将所有修改过的块缓冲区排入写队列,然后就返回
  • fsync 当前 fd 的数据,及时写入磁盘,完成之后才会返回
  • fdatasync 和fsync类似,不过还更新 文件属性

函数 fcntl

fcntl 函数可以改变一打开文件的属性。

1
2
3
#include<fcntl.h>
int fcntl(int fd, int cmd, ... /*int arg */);
// 返回值:成功 ,返回见下表 出错 返回 -1

fcntl函数有5种功能:

  1. 复制一个现有的描述符(cmd=F_DUPFD).
  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).

    cmd 选项:

F_DUPFD 返回一个如下描述的(文件)描述符:

  • 最小的大于或等于arg的一个可用的描述符

  • 与原始操作符一样的某对象的引用         

  • 如果对象是文件(file)的话,返回一个新的描述符,这个描述符与arg共享相同的偏移量(offset)

  • 相同的访问模式(读,写或读/写)

  • 相同的文件状态标志(如:两个文件描述符共享相同的状态标志)

  • 与新的文件描述符结合在一起的close-on-exec标志被设置成交叉式访问execve(2)的系统调用

F_GETFD 取得与文件描述符fd联合close-on-exec标志,类似FD_CLOEXEC.如果返回值和FD_CLOEXEC进行与运算结果是0的话,文件保持交叉式访问exec(), 否则如果通过exec运行的话,文件将被关闭(arg被忽略)

F_SETFD 设置close-on-exec旗标。该旗标以参数arg的FD_CLOEXEC位决定。

F_GETFL 取得fd的文件状态标志,如同下面的描述一样(arg被忽略)

F_SETFL 设置给arg描述符状态标志,可以更改的几个标志是:O_APPEND, O_NONBLOCK,O_SYNC和O_ASYNC。

F_GETOWN 取得当前正在接收SIGIO或者SIGURG信号的进程id或进程组id,进程组id返回成负值(arg被忽略)

F_SETOWN 设置将接收SIGIO和SIGURG信号的进程id或进程组id,进程组id通过提供负值的arg来说明,否则,arg将被认为是进程id

fcntl的返回值: 与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。下列三个命令有特定返回值:F_DUPFD,F_GETFD,F_GETFL以及F_GETOWN。第一个返回新的文件描述符,第二个返回相应标志,最后一个返回一个正的进程ID或负的进程组ID。

参考资料

应用实例:

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
#include "apue.h"
#include <fcntl.h>

int
main(int argc, char *argv[])
{
int val;

if (argc != 2)
err_quit("usage: a.out <descriptor#>");

if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0)
err_sys("fcntl error for fd %d", atoi(argv[1]));

switch (val & O_ACCMODE) {
case O_RDONLY:
printf("read only");
break;

case O_WRONLY:
printf("write only");
break;

case O_RDWR:
printf("read write");
break;

default:
err_dump("unknown access mode");
}

if (val & O_APPEND)
printf(", append");
if (val & O_NONBLOCK)
printf(", nonblocking");
if (val & O_SYNC)
printf(", synchronous writes");

#if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC)
if (val & O_FSYNC)
printf(", synchronous writes");
#endif

putchar('\n');
exit(0);
}

函数 ioctl

I/O 操作杂货箱

1
2
3
4
#include <sys/ioctl.h> /* BSD and Linux */
#include <unistd.h> /* System V */

int ioctl(int fd, int request, ...);

TODO: 后面会有详解,这里先不在详细说明

/dev/fd

文件标识符

MISC

  1. 关于lseek 与 append 的冲突

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include<unistd.h>
    #include<fcntl.h>
    #include<stdio.h>
    int main(){
    int fd = open("./test", O_RDWR | O_APPEND, 0770);
    printf("%d\n>>>",lseek(fd,-35, SEEK_END));
    write(fd,"********",10);
    return 0;
    }
    // 结果,只能把内容写到文件末尾

    2.fork 中 father 与 child 的运行顺序

    1. 先运行完 father 然后再运行 child 除非 father wait
    2. write 函数,不管数组是否越界,只是个写

dw文件I/O