Linux下C语言教程-李慧芹老师-第十三章

配套代码笔记仓库

目录

I/O操作

输入输出是一切实现的基础。

  • 标准IO:stdio

  • 系统调用IO(文件IO):sysio

优先使用标准IO,兼容性更好,还有合并系统调用的优势。

标准IO

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
/* stdio */
/* FILE类型贯穿始终 */

FILE *fopen(const char *path, const char *mode);
/**
* fopen 返回指针的储存位置? 1.栈 2.静态区 3.堆
* 正确答案:3.堆。
* 因为如果是栈,就是函数内部局部变量,无法返回地址。
* 如果是静态区,无法确定需要多少个这个变量。
*
* 只有 r 和 r+ 一定要求文件存在
* 另外几种不存在会创建
*
* 创建文件的权限
* 0666 & ~umask
*
* 对于普通用户
* umask 得到 022
*
*/
int fclose(FILE *fp);

int fputc(int c, FILE *stream);
int fgetc(FILE *stream);

char *fgets(char *s, int size, FILE *stream);
/**
* 两种正常返回的情况:
* 1. 读了 size-1 个字节,最后一个字节留给 '\0'
* 2. 读到了 '\n'
*
* eg. 加入用fgets(buf, 5, stream) 来读 abcd
* 是会读两次的
* 第一次:abcd'\0'
* 第二次:'\n''\0'
*/
int fputs(const char *restrict s, FILE *restrict stream);

/**
* 这一对函数常用但是无法验证边界
* 尽量一次只读单字节,更安全
*
* 返回值:成功读/写的对象的数量
*/
size_t fread(void *ptr, size_t size, size_t nemmb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);


int printf(const char *restrict format, ...);
/**
* 常用于 fprintf(stderr,...)
*/
int fprintf(FILE *restrict stream, const char *restrict format, ...);
int dprintf(int fd, const char *restrict format, ...);

/**
* 将格式化内容输出到一个字符串
*
* 和 atoi() 正好相反
*
*/
int sprintf(char *restrict str, const char *restrict format, ...);

/**
* 比sprintf多了size参数,更安全
*/
int snprintf(char str[restrict.size],
size_t size,
const char *restrict format,
...)

// !!! 慎用%s
int scanf(const char *restrict format, ...);
int fscanf(FILE *restrict stream,
const char *restrict format, ...);


/**
* 移动文件当前位置指针
*
* 可用于生成空洞文件,下载器原理
*
* @prarm: offset 移动多远
* @prarm: whence 移动方向
* SEEK_SET, SEEK_CUR, SEEK_END
*
* @return 成功0,失败-1
*/
int fseek(FILE *stream, long offset, int whence);

/**
* 反映当前文件指针所在位置
*
* 这个long的负值部分无法使用。
* 所以文件无法超过2G。
*
*/
long ftell(FILE *stream);

/**
* 解决上面long的问题。
*
* 最好编译时加上
* #define _FILE_OFFSET_BITS 64
* 可以写入makefile
*
* 但是这俩函数是方言,前面那个long的一对支持C89,C99
*
*/
int fseeko(FILE *stream, off_t offset, int whence);
off_t ftello(FILE *stream);

/**
* 将文件指针置于文件首
* equivalent to:
* fseek(stream, 0L, SEEK_SET);
*/
void rewind(FILE *stream);

/**
* 缓冲区的作用:
* 大多数情况下是好事,合并系统调用
*
* 行缓冲: 换行时候刷新,满了的时候刷新,强制刷新(标准输出是这样的,因为是终端设备)
*
* 全缓冲: 满了的时候刷新,强制刷新(默认,只要不是终端设备)
*
* 无缓冲: 如stderr,需要立即输出的内容
*/
fflush();

/**
* @prarm: mode
* 三种缓冲模式:
* _IONBF
* _IOLBF
* _IOFBF
*/
int setvbuf(FILE *stream, char *buf, int mode, size_t size);

/**
* 为了读取一行
*
* 使用办法:
* #define _GNU_SOURCE 这个不想写到代码里面的话可以写到makefile
* eg. CFLAGS+=-D_FILE_OFFSET_BITS=64 -D_GNU_SOURCE
* #include <stdio.h>
*
* !!! 里面有 malloc 动作,未释放
* !!! 是方言,可以自己封装一个mygetline和mygetline_free
* !!! 但是根据chatgpt,好像直接 free(*lineptr) 就行了
*
*/
ssize_t getline(char **lineptr, size_t *n, FILE *stream);

/**
* 临时文件
* 1. 如何不冲突的创建
* 2. 及时销毁
*
* tmpnam: 创建临时文件名字
* 有并发危险,因为产生名字和创建文件是两步
*
* tmpfile: 创建临时文件
* 是匿名文件,ls -a 都看不到
* 避免冲突
*/
char *tmpnam(char *s);
FILE *tmpfile(void);

文件IO/系统调用IO

文件描述符(fd)是在文件IO中贯穿始终的类型。

文件描述符的概念

是一个整型数,是一个指针数组的下标。

优先使用当前可用范围内最小的。

文件IO操作相关函数:

  • open
  • close
  • read
  • write
  • lsee

可以使用./open file &来后台运行一个程序。

然后通过ps查看进程号

然后进入/proc/进程号/fd查看文件描述符

前三个是标准输入、输出、错误,后面的是打开的文件描述符

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
/**
* flag:
*
* r -> O_RDONLY
* r+ -> O_RDWR
* w -> O_WRONLY | O_CREAT | O_TRUNC
* w+ -> O_RDWR | O_TRUNC | O_CREAT
*
* O_RDONLY 只读
* O_WRONLY 只写
* O_RDWR 读写
* O_CREAT 创建
* O_TRUNC 截断
* O_APPEND 追加
* O_EXCL 排他(若要创建的文件已存在则报错)
* O_NONBLOCK 非阻塞
* O_SYNC 同步
* O_DSYNC 数据同步
* O_RSYNC 读同步
* O_DIRECT 直接IO
* O_LARGEFILE 大文件
* O_DIRECTORY 目录
* O_NOFOLLOW 不跟踪符号链接
* O_CLOEXEC close-on-exec
* O_PATH 仅打开目录
* O_TMPFILE 临时文件
* O_NOCTTY 不分配控制终端
*
* 如果有creat就必须用三参数的形式
* C语言没有重载,这是变参函数
*
* @prarm: pathname 文件路径
* @prarm: flags 文件打开方式
* @prarm: mode 文件权限
* 假如0666,就是rw-rw-rw-,110110110
*
*/
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

int close(int fd);

/**
* @return 读取的字节数,失败返回-1
*/
ssize_t read(int fd, void *buf, size_t count);

/**
* 想要控制写入的位置,需要使用lseek
*
* @return 写入的字节数,失败返回-1
*/
ssize_t write(int fd, const void *buf, size_t count);

/**
* 移动文件指针
*
* @prarm: offset 移动多远
* @prarm: whence 移动方向
* SEEK_SET, SEEK_CUR, SEEK_END
*
* @return 成功0,失败-1
*/
off_t lseek(int fd, offt offset, int whence);

例题:通过文件IO处理csv表格

1
2
3
4
,语文,数学,英语,总分,评价
张三,90,91,92,,
李四,80,81,82,,
王五,70,71,72,,

思路:逐行处理

可以使用16进制查看工具

文件IO与标准IO的区别

区别:响应速度&吞吐量

文件IO需要频繁进入内核,标准IO通过缓冲区合并系统调用。

响应速度快就文件IO,吞吐量大就标准IO。

[!warning]
二者不可混用

转换方法:fileno, fdopen

IO的效率问题

习题

mycpy.c程序进行更改,将BUFSIZE的值放大,观察进程消耗的时间,注意性能出现拐点的值以及程序何时段错误。

解答

BUFSIZE作为命令行参数传入,int bufsize = atoi(argv[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
#!/bin/bash

# 生成一个 5GB 的文件
dd if=/dev/urandom of=/tmp/bigfile bs=1G count=5

# 输入和输出文件的路径
src="/tmp/bigfile"
dst="/tmp/outfile"

# 编译你的程序
gcc -o mycpy_bufsize mycpy_bufsize.c

# 初始化 BUFSIZE
bufsize=512

# 循环,每次 BUFSIZE * 2
while true; do
# 用 time 命令运行你的程序,并将结果重定向到一个临时文件
{ time ./mycpy_bufsize $src $dst $bufsize; } 2> time.txt

# 检查程序的退出状态
if [ $? -ne 0 ]; then
echo "Max BUFSIZE before segfault: $bufsize"
break
fi

# 提取 time 的结果
real_time=$(grep real time.txt | awk -F' ' '{print $2}')
user_time=$(grep user time.txt | awk -F' ' '{print $2}')
sys_time=$(grep sys time.txt | awk -F' ' '{print $2}')

# 输出 BUFSIZE 和 time 的结果
echo "BUFSIZE: $bufsize, Real Time: $real_time, User Time: $user_time, Sys Time: $sys_time"

# BUFSIZE * 2
bufsize=$((bufsize * 2))
done

# 删除临时文件
rm time.txt
rm $src
rm $dst

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
wan@SK-20240106UQUX:~/Linux-C-Notes/C13-Linux系统编程/io/sys$ ./time.sh
BUFSIZE: 512, Real Time: 0m7.672s, User Time: 0m0.650s, Sys Time: 0m7.007s
BUFSIZE: 1024, Real Time: 0m5.026s, User Time: 0m0.201s, Sys Time: 0m4.651s
BUFSIZE: 2048, Real Time: 0m3.535s, User Time: 0m0.158s, Sys Time: 0m3.183s
BUFSIZE: 4096, Real Time: 0m2.418s, User Time: 0m0.059s, Sys Time: 0m2.232s
BUFSIZE: 8192, Real Time: 0m2.363s, User Time: 0m0.040s, Sys Time: 0m2.150s
BUFSIZE: 16384, Real Time: 0m2.279s, User Time: 0m0.030s, Sys Time: 0m2.079s
BUFSIZE: 32768, Real Time: 0m2.238s, User Time: 0m0.020s, Sys Time: 0m2.026s
BUFSIZE: 65536, Real Time: 0m2.114s, User Time: 0m0.000s, Sys Time: 0m1.972s
BUFSIZE: 131072, Real Time: 0m2.302s, User Time: 0m0.019s, Sys Time: 0m1.982s
BUFSIZE: 262144, Real Time: 0m2.244s, User Time: 0m0.000s, Sys Time: 0m2.016s
BUFSIZE: 524288, Real Time: 0m2.254s, User Time: 0m0.000s, Sys Time: 0m2.039s
BUFSIZE: 1048576, Real Time: 0m2.249s, User Time: 0m0.010s, Sys Time: 0m2.037s
BUFSIZE: 2097152, Real Time: 0m2.304s, User Time: 0m0.000s, Sys Time: 0m2.108s
BUFSIZE: 4194304, Real Time: 0m2.234s, User Time: 0m0.010s, Sys Time: 0m2.082s
Max BUFSIZE before segfault: 8388608

ulimit -a中,我的系统的stack size8192,所以BUFSIZE不能超过8192,否则会段错误。与测试结果一致。

文件共享

多个任务共同操作一个文件或者协同完成任务

面试题:写程序删除一个文件的第10行

补充函数:

1
2
3
// 截断文件到某长度
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 最简单思路,将11行开始的内容到第10行开始处覆盖写
while()
{
lseek 11 + read +lseek 10 + write
}

// 优化思路,两个文件描述符,一个读一个写
1 -> open r -> fd1 -> lseek 11
2 -> open r+ -> fd2 -> lseek 10

while()
{
1->fd1-> read
2->fd2-> write
}

// 两个进程, 设计进程间通信
process1 -> open -> r
process2 -> open -> r+

p1->read -> p2->write

原子操作

指不可分割的操作

作用:解决竞争和冲突

tmpnam函数,产生文件名和创建文件是两步,会有并发问题。

程序中的重定向:dup, dup2

1
2
3
4
5
6
7
/**
* dup 和 dup2 都是复制文件描述符
* dup2 可以指定新的文件描述符
* dup 会返回一个新的文件描述符
*/
int dup(int oldfd);
int dup2(int oldfd, int newfd);

同步

同步内核层面的buffer和cache

1
2
3
4
5
6
7
8
9
10
void sync(void);
int fsync(int fd);
int fdatasync(int fd); // 只刷新数据,不刷新亚数据

// 文件描述符所有的操作几乎都来源于该函数
int fcntl(int fd, int cmd, ... /* arg */);

// 设备相关的内容
int ioctl(int fd, unsigned long request, ... /* arg */);

/dev/fd/目录

虚目录:显示当前进程的文件描述符信息


Linux下C语言教程-李慧芹老师-第十三章
http://sinlatansen.github.io/2024/05/16/Linux下C语言教程-李慧芹老师-第十三章/
作者
fugu
发布于
2024年5月16日
许可协议