文件大小基础知识
给定字符串:line_0001,共 9 个字符,9 字节
expr length line_0001
9
给定空文本文件:touch test.txt
du -sb test.txt
0 test.txt
将字符串写入空文本文件中,有 2 种情景
# 不加换行符,文件大小为 9 字节
echo -n line_0001 > test.txt
# 加换行符,文件大小为 10 字节
echo -n line_0001 > test.txt
可知换行符\n
只占用 1 个字节,du 命令获取到的文件大小就是文件内容本身的字节数总和,并没有包含 fs 的元数据
文件读取基础知识
将 9 个字符写入空文本文件中,加上换行符,文件大小为 10 字节
echo line_0001 > test.txt
使用 strace 来跟踪 cat 命令系统调用和信号
strace -v cat test.txt 1>/dev/null
- -v:打印完成的系统调用信息,默认会进行缩减,以缩短每行的内容
- 1>/dev/null:把标准输出的内容扔掉,避免干扰阅读 strace 的系统调用信息
# 打开文件
open("test.txt", O_RDONLY) = 3
# 获取文件状态
fstat(3, {st_dev=makedev(8, 4), st_ino=4980738, st_mode=S_IFREG|0644, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=8, st_size=10, st_atime=2021/09/28-15:47:48, st_mtime=2021/09/28-16:43:39, st_ctime=2021/09/28-16:43:39}) = 0
fadvise64(3, 0, 0, POSIX_FADV_SEQUENTIAL) = 0
# 第一次读取文件内容,buffer 为 65535 字节,即每次最多从文件读取的数据量;这里只读到 10 字节的内容
read(3, "line_no_0001\n", 65536) = 10
# 调用 write 把数据写到信号为 1 的进程,即把内容输出到标准输出
write(1, "line_0001\n", 13) = 10
# 第二次读取文件内容,为空,所以就结束了 read 这个系统调用
read(3, "", 65536) = 0
close(3) = 0
# 结束信号为 1 的系统调用
close(1) = 0
打印指定行号内容
生成文本文件
> test.txt; for i in {0001..6554}; do echo "line_$i" >> test.txt; done
line_0001
line_0002
line_0003
...
line_6552
line_6553
line_6554
查看文件状态
stat test.txt
File: ‘test.txt’
Size: 65540 Blocks: 136 IO Block: 4096 regular file
Device: 804h/2052d Inode: 4980738 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2021-09-28 15:47:48.825610568 +0800
Modify: 2021-09-28 19:47:05.209576214 +0800
Change: 2021-09-28 19:47:05.209576214 +0800
Birth: -
item | desc |
---|---|
Size | 每行内容长度:65540/6554 = 10 字节 |
Blocks | 理论上只需要 2 个 block 即可存储整个文件的内容,但实际上数据存放在 136 个</br> block 上了 |
Inode | 4980738 是该文件的入口 inode id |
方式一:使用 sed 命令打印 405-415 行的内容
sed:stream editor
sed -n '405,415p' test.txt
line_0405
line_0406
line_0407
line_0408
line_0409
line_0410
line_0411
line_0412
line_0413
line_0414
line_0415
目标已达成,使用 strace 跟踪一下 sed 的操作
strace -vy sed -n '405,415p' test.txt 1>/dev/null
# 因为是 ssh 登陆但,所以是通过伪终端 /dev/pts/1 输入命令,后面会有一个 close pts 设备的动作
execve("/usr/bin/sed", ["sed", "-n", "405,415p", "test.txt"], ..., "SSH_TTY=/dev/pts/1", ...) = 0
...
# 打开文本文件 test.txt,通过 man open 得知函数的定义为:
# int open(const char *pathname, int flags);
# 参数 flags 就是 access modes: O_RDONLY, O_WRONLY, or O_RDWR
open("test.txt", O_RDONLY) = 3
# 获取文件的状态,数据存放在 136 个 block 中,获取的文件信息和 stat 命令的一致
# 通过 man fstat 得知,fstat 完全等同于 stat,数据结构如下:
# struct stat {
# dev_t st_dev; /* ID of device containing file */
# ino_t st_ino; /* inode number */
# mode_t st_mode; /* protection */
# nlink_t st_nlink; /* number of hard links */
# uid_t st_uid; /* user ID of owner */
# gid_t st_gid; /* group ID of owner */
# dev_t st_rdev; /* device ID (if special file) */
# off_t st_size; /* total size, in bytes */
# blksize_t st_blksize; /* blocksize for file system I/O */
# blkcnt_t st_blocks; /* number of 512B blocks allocated */
# time_t st_atime; /* time of last access */
# time_t st_mtime; /* time of last modification */
# time_t st_ctime; /* time of last status change */
# };
fstat(3</data/masoncheng/tmp/test.txt>, {st_dev=makedev(8, 4), st_ino=4980738, st_mode=S_IFREG|0644, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=136, st_size=65540, st_atime=2021/09/28-15:47:48, st_mtime=2021/09/29-14:47:01, st_ctime=2021/09/29-14:47:01}) = 0
# 为 test.txt 创建 mmap 的内存映射地址:
# mmap:将文件映射到内存地址,读取文件时直接通过内存地址读取,绕过磁盘寻址,提升读性能。通过命令 man mmap 得知函数定义为:
# void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
# sed 读文件前,都先用 mmap 把文件映射到内存,cat 没有 mmap 操作,理论上 sed 的读取性能比 cat 更好,实则不然
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2e54258000
# 第一次 read:
# read 的 buffer 根据 os 的 block size 设置,默认为 4096 字节,每次最多读取 4K 个的字符,减少读取次数
# 每次 read 的数据是 4096 bytes,那么共读取:16 + 1 次(65540 = 16*4096 + 4)才能读完数据,读完后,还会额外读取一次,最后一次为读到的数据为空,这时才结束读取操作
read(3</data/masoncheng/tmp/test.txt>, "line_0001\nline_0002\nline_0003\nli"..., 4096) = 4096
# 因为要将指定行数的数据输出到 /dev/null,所以不管第一次 read 是否有输出,都要要获取 /dev/null 设备的文件信息;
# 这里仅获取设备信息,不做任何输出,要等 sed 遍历完整个文件才输出,这个点需要优化
fstat(1</dev/null>, {st_dev=makedev(0, 5), st_ino=674671422, st_mode=S_IFCHR|0666, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=0, st_rdev=makedev(1, 3), st_atime=2018/08/20-11:39:20, st_mtime=2018/08/20-11:39:20, st_ctime=2018/08/20-11:39:20}) = 0
# 通过命令 man ioctl 发现,这是个用来控制设备的函数,函数定义为:int ioctl(int d, int request, ...);
ioctl(1</dev/null>, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7ffec3c33a60) = -1 EACCES (Permission denied)
# mmap 映射 /dev/null 设备到内存中
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2e54257000
# 第 2-16 次 read
read(3</data/masoncheng/tmp/test.txt>, "410\nline_0411\nline_0412\nline_041"..., 4096) = 4096
read(3</data/masoncheng/tmp/test.txt>, "ne_0820\nline_0821\nline_0822\nline"..., 4096) = 4096
read(3</data/masoncheng/tmp/test.txt>, "9\nline_1230\nline_1231\nline_1232\n"..., 4096) = 4096
read(3</data/masoncheng/tmp/test.txt>, "_1639\nline_1640\nline_1641\nline_1"..., 4096) = 4096
read(3</data/masoncheng/tmp/test.txt>, "line_2049\nline_2050\nline_2051\nli"..., 4096) = 4096
read(3</data/masoncheng/tmp/test.txt>, "458\nline_2459\nline_2460\nline_246"..., 4096) = 4096
read(3</data/masoncheng/tmp/test.txt>, "ne_2868\nline_2869\nline_2870\nline"..., 4096) = 4096
read(3</data/masoncheng/tmp/test.txt>, "7\nline_3278\nline_3279\nline_3280\n"..., 4096) = 4096
read(3</data/masoncheng/tmp/test.txt>, "_3687\nline_3688\nline_3689\nline_3"..., 4096) = 4096
read(3</data/masoncheng/tmp/test.txt>, "line_4097\nline_4098\nline_4099\nli"..., 4096) = 4096
read(3</data/masoncheng/tmp/test.txt>, "506\nline_4507\nline_4508\nline_450"..., 4096) = 4096
read(3</data/masoncheng/tmp/test.txt>, "ne_4916\nline_4917\nline_4918\nline"..., 4096) = 4096
read(3</data/masoncheng/tmp/test.txt>, "5\nline_5326\nline_5327\nline_5328\n"..., 4096) = 4096
read(3</data/masoncheng/tmp/test.txt>, "_5735\nline_5736\nline_5737\nline_5"..., 4096) = 4096
read(3</data/masoncheng/tmp/test.txt>, "line_6145\nline_6146\nline_6147\nli"..., 4096) = 4096
read(3</data/masoncheng/tmp/test.txt>, "554\n", 4096) = 4
# 最后一次 read,读到的内容为空
read(3</data/masoncheng/tmp/test.txt>, "", 4096) = 0
# 关闭 test.txt 文件
close(3</data/masoncheng/tmp/test.txt>) = 0
# 删除 test.txt 文件的 mmap 映射
munmap(0x7f2e54258000, 4096) = 0
# 将缓存中的数据写入到 /dev/null 设备
write(1</dev/null>, "line_0405\nline_0406\nline_0407\nli"..., 110) = 110
# 关闭 /dev/null 设备的文件描述符
close(1</dev/null>) = 0
# 删除 /dev/null 设备的 mmap 映射
munmap(0x7f2e54257000, 4096) = 0
# 关闭 /dev/pts/1 伪终端文件,表示字符界面的字符输入已完成
# 通过 man pts 发现,这里获取了一对伪终端的主备设备信息(伪终端就是我们平常常用的 xterm、minicom 等字符终端):
# /dev/ptmx:特殊字符设备,伪终端的 master 只有 1 个
# /dev/pts/1:特殊字符设备,伪终端的 slave,slave 有很多个
close(2</dev/pts/1>) = 0
exit_group(0) = ?
octl(1</dev/null>, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7ffec3c33a60) = -1 EACCES (Permission denied)
# 命令执行成功,返回状态码 0
+++ exited with 0 +++
优化一下,在打印完指定行后就退出,不再继续读取下去,再对比下 strace 信息
strace -vy sed -n '405,415p;415q' test.txt 1>/dev/null
open("test.txt", O_RDONLY) = 3
fstat(3</data/masoncheng/tmp/test.txt>, {st_dev=makedev(8, 4), st_ino=4980738, st_mode=S_IFREG|0644, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=136, st_size=65540, st_atime=2021/09/28-15:47:48, st_mtime=2021/09/29-16:48:58, st_ctime=2021/09/29-16:48:58}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3825a5c000
# 第一次读
read(3</data/masoncheng/tmp/test.txt>, "line_0001\nline_0002\nline_0003\nli"..., 4096) = 4096
fstat(1</dev/null>, {st_dev=makedev(0, 5), st_ino=674671422, st_mode=S_IFCHR|0666, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=0, st_rdev=makedev(1, 3), st_atime=2018/08/20-11:39:20, st_mtime=2018/08/20-11:39:20, st_ctime=2018/08/20-11:39:20}) = 0
ioctl(1</dev/null>, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7ffe73911900) = -1 EACCES (Permission denied)
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3825a5b000
# 第二次读
read(3</data/masoncheng/tmp/test.txt>, "410\nline_0411\nline_0412\nline_041"..., 4096) = 4096
close(3</data/masoncheng/tmp/test.txt>) = 0
munmap(0x7f3825a5c000, 4096) = 0
write(1</dev/null>, "line_0405\nline_0406\nline_0407\nli"..., 110) = 110
close(1</dev/null>) = 0
munmap(0x7f3825a5b000, 4096) = 0
close(2</dev/pts/60>) = 0
exit_group(0) = ?
+++ exited with 0 +++
可以发现,read 只执行 2 次:
- 第一个 4096 字节的内容,只到读到 4096/10 = 409 行(409 行的内容只读到 2 个字符)
- 第二个 4096 字节的内容,读到 8192/10 = 819 行(819 行的内容读到 8 个字符)
方式二:使用 awk 打印 405-415 行的内容
命令:
awk '(NR>=405 && NR <= 415) {print}' test.txt
使用 strace 发现,和 sed 一样,打印完指定行内容后也是继续读取剩余内容,这里也同样可以做优化:
awk '(NR>=405 && NR <= 415) {print; exit}' test.txt
awk 的 read buffer 也是使用 os 的 block size
更多用法
参考:https://www.cnblogs.com/daodaotest/p/13277208.html
生成测试文本内容
seq -f "%02g daodaotest" 1 10 > test.txt
查看测试文本内容,并显示行号
cat -n test.txt
1 01 daodaotest
2 02 daodaotest
3 03 daodaotest
4 04 daodaotest
5 05 daodaotest
6 06 daodaotest
7 07 daodaotest
8 08 daodaotest
9 09 daodaotest
10 10 daodaotest
awk '{print NR" "$0}' test.txt
1 01 daodaotest
2 02 daodaotest
3 03 daodaotest
4 04 daodaotest
5 05 daodaotest
6 06 daodaotest
7 07 daodaotest
8 08 daodaotest
9 09 daodaotest
10 10 daodaotest
打印前 5 行内容
head -n 5 test.txt
sed -n '1,5p' test.txt
awk 'NR<6' test.txt
打印第 5 行内容
sed -n '5p' test.txt
awk 'NR==5' test.txt
tail -n +5 test.txt | head -1
打印 5~10 行内容
sed -n '5,10p' test.txt
awk 'NR>4 && NR<11' test.txt
tail -n +5 test.txt | head -6
打印第 3 行 和 5~7 行内容
sed -n '3p;5,7p' test.txt
awk 'NR==3 || (NR>4 && NR<8)' test.txt
打印奇偶行内容
# NR 表示行号
awk 'NR%2!=0' test.txt
awk 'NR%2' test.txt
## i 为变量,未定义变量初始值为 0,对于字符运算,未定义变量初值为空字符串
## 读取第 1 行记录,进行模式匹配:i=!0(!表示取反)。! 右边是个布尔值,0 为假,非 0 为真,!0 就是真,因此 i=1,条件为真打印第一条记录。
## 读取第 2 行记录,进行模式匹配:i=!1(因为上次 i 的值由 0 变成了 1),条件为假不打印。
## 读取第 3 行记录,因为上次条件为假,i 恢复初值为 0,继续打印。以此类推...
## 上述运算并没有真正的判断记录,而是布尔值真假判断。
$ awk 'i=!i' test.txt
## m~np:m 表示起始行;~2 表示:步长
$ sed -n '1~2p' test.txt
## 先打印第 1 行,执行 n 命令读取当前行的下一行,放到模式空间,后面再没有打印模式空间行操作,所以只保存不打印,同等方式继续打印第 3 行。
$ sed -n '1,$p;n' test.txt
$ sed -n 'p;n' test.txt
# 打印偶数行内容
$ awk 'NR%2==0' test.txt
$ awk '!(NR%2)' test.txt
$ awk '!(i=!i)' test.txt
$ sed -n 'n;p' test.txt
$ sed -n '1~1p' test.txt
$ sed -n '1,$n;p' test.txt
打印后 5 行内容
tail -n 5 test.txt
打印最后一行内容
tail -n 1 test.txt
sed -n '$p' test.txt
awk 'END {print}' test.txt
打印以 "1" 开头的行内容
sed -n '/^1/p' test.txt
grep "^1" test.txt
打印不以 "1" 开头的行内容
$ sed -n '/1/!p' test.txt
$ grep -v "^1" test.txt
从匹配 "03" 行到第 5 行内容
sed -n '/03/,5p' test.txt
打印匹配 "03" 行 到匹配 "05" 行内容
sed -n '/03/,/05/p' test.txt