Ubuntu 16.04/x86_64
sudo apt install binutils build-essential sysstat
hello.c
#include <stdio.h>
int main(void){
puts("hello, world");
return 0;
}
print("hello, world")
cc -o hello hello.c
#./hello
hello, world
# strace -o hello.log ./hello
hello, world
# cat hello.log
# strace -T -o hello_t.log ./hello # 带上耗时
# strace -T -o hello.py.log python3 hello.py
sar -P ALL 1 # 查看各模式下的运行时间
Linux 5.4.0-42-generic (VM-4-14-ubuntu) 10/19/2022 _x86_64_ (2 CPU)
10:22:31 PM CPU %user %nice %system %iowait %steal %idle
10:22:32 PM all 0.51 0.00 0.00 0.00 0.00 99.49
10:22:32 PM 0 1.01 0.00 0.00 0.00 0.00 98.99
10:22:32 PM 1 0.00 0.00 0.00 0.00 0.00 100.00
10:22:32 PM CPU %user %nice %system %iowait %steal %idle
10:22:33 PM all 8.54 0.00 0.50 0.50 0.00 90.45
10:22:33 PM 0 9.00 0.00 1.00 0.00 0.00 90.00
10:22:33 PM 1 8.08 0.00 0.00 1.01 0.00 90.91
用户态无限循环
cat loop.c
int main(void){
for(;;)
;
}
cc -o loop loop.c
./loop &
[1] 224242
sar -P ALL 1
Linux 5.4.0-42-generic (VM-4-14-ubuntu) 10/19/2022 _x86_64_ (2 CPU)
10:29:53 PM CPU %user %nice %system %iowait %steal %idle
10:29:54 PM all 60.20 0.00 0.50 0.50 0.00 38.81
10:29:54 PM 0 20.79 0.00 0.99 0.99 0.00 77.23
10:29:54 PM 1 100.00 0.00 0.00 0.00 0.00 0.00
10:29:54 PM CPU %user %nice %system %iowait %steal %idle
10:29:55 PM all 51.26 0.00 0.50 0.00 0.00 48.24
10:29:55 PM 0 2.02 0.00 1.01 0.00 0.00 96.97
10:29:55 PM 1 100.00 0.00 0.00 0.00 0.00 0.00
10:29:55 PM CPU %user %nice %system %iowait %steal %idle
10:29:56 PM all 50.25 0.00 0.00 0.00 0.00 49.75
10:29:56 PM 0 0.00 0.00 0.00 0.00 0.00 100.00
10:29:56 PM 1 100.00 0.00 0.00 0.00 0.00 0.00
大量内核态
cat ppidloop.c
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
for(;;)
getppid();
}
cc -o ppidloop ppidloop.c
./ppidloop &
[1] 225005
sar -P ALL 1 1
Linux 5.4.0-42-generic (VM-4-14-ubuntu) 10/19/2022 _x86_64_ (2 CPU)
10:35:01 PM CPU %user %nice %system %iowait %steal %idle
10:35:02 PM all 16.67 0.00 35.86 0.00 0.00 47.47
10:35:02 PM 0 2.04 0.00 2.04 0.00 0.00 95.92
10:35:02 PM 1 31.00 0.00 69.00 0.00 0.00 0.00
Average: CPU %user %nice %system %iowait %steal %idle
Average: all 16.67 0.00 35.86 0.00 0.00 47.47
Average: 0 2.04 0.00 2.04 0.00 0.00 95.92
Average: 1 31.00 0.00 69.00 0.00 0.00 0.00
系统调用的标准库及POSIX标准中定义的函数
通常会以GNU项目提供的glibc作为C标准库使用
ldd用于查看程序所依赖的库
$ ldd /bin/echo
linux-vdso.so.1 (0x00007ffd37dbe000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc36d43a000)
/lib64/ld-linux-x86-64.so.2 (0x00007fc36d640000)
$ ldd /usr/bin/python3
linux-vdso.so.1 (0x00007ffe3ea61000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6093a1f000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f60939fc000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f60939f6000)
libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007f60939f1000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f60938a2000)
libexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007f6093874000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f6093856000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6093c1a000)
Linux提供了fork()函数和execve()函数,其底层分别调用clone()和execve()的系统调用
fork.c
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<err.h>
static void child(){
printf("I'm child my pid is %d. \", getpid());
exit(EXIT_SUCCESS);
}
static void parent(pid_t pid_c){
printf("I'm parent my pid is %d and the pid of my child is %d. \", getpid(), pid_c);
exit(EXIT_SUCCESS);
}
int main(void){
pid_t ret;
ret = fork();
if(ret == -1)
err(EXIT_FAILURE, "fork failed");
if(ret == 0){
// fork()会返回0给子进程
child();
}else{
//fork()会返回新创建的子进程的ID
parent(ret);
}
err(EXIT_FAILURE, "shouldn't reach here");
}
linux可执行文件的结构遵循名为ELF(Executable and Linkable Format)
ubuntu@VM-4-14-ubuntu:~$ readelf -h /usr/bin/sleep
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x2850
Start of program headers: 64 (bytes into file)
Start of section headers: 37336 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 29
代码段和数据段在文件中的偏移量、大小和起始位置。
ubuntu@VM-4-14-ubuntu:~$ readelf -S /usr/bin/sleep
There are 30 section headers, starting at offset 0x91d8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.propert NOTE 0000000000000338 00000338
0000000000000020 0000000000000000 A 0 0 8
[ 3] .note.gnu.build-i NOTE 0000000000000358 00000358
0000000000000024 0000000000000000 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000000037c 0000037c
0000000000000020 0000000000000000 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000000003a0 000003a0
00000000000000a8 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 0000000000000448 00000448
0000000000000600 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000000a48 00000a48
000000000000031f 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 0000000000000d68 00000d68
0000000000000080 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000000de8 00000de8
0000000000000060 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000000e48 00000e48
00000000000002b8 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000001100 00001100
00000000000003f0 0000000000000018 AI 6 25 8
[12] .init PROGBITS 0000000000002000 00002000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000002020 00002020
00000000000002b0 0000000000000010 AX 0 0 16
[14] .plt.got PROGBITS 00000000000022d0 000022d0
0000000000000010 0000000000000010 AX 0 0 16
[15] .plt.sec PROGBITS 00000000000022e0 000022e0
00000000000002a0 0000000000000010 AX 0 0 16
[16] .text PROGBITS 0000000000002580 00002580
0000000000003692 0000000000000000 AX 0 0 16
[17] .fini PROGBITS 0000000000005c14 00005c14
000000000000000d 0000000000000000 AX 0 0 4
[18] .rodata PROGBITS 0000000000006000 00006000
0000000000000f6c 0000000000000000 A 0 0 32
[19] .eh_frame_hdr PROGBITS 0000000000006f6c 00006f6c
00000000000002b4 0000000000000000 A 0 0 4
[20] .eh_frame PROGBITS 0000000000007220 00007220
0000000000000d18 0000000000000000 A 0 0 8
[21] .init_array INIT_ARRAY 0000000000009bb0 00008bb0
0000000000000008 0000000000000008 WA 0 0 8
[22] .fini_array FINI_ARRAY 0000000000009bb8 00008bb8
0000000000000008 0000000000000008 WA 0 0 8
[23] .data.rel.ro PROGBITS 0000000000009bc0 00008bc0
00000000000000b8 0000000000000000 WA 0 0 32
[24] .dynamic DYNAMIC 0000000000009c78 00008c78
00000000000001f0 0000000000000010 WA 7 0 8
[25] .got PROGBITS 0000000000009e68 00008e68
0000000000000190 0000000000000008 WA 0 0 8
[26] .data PROGBITS 000000000000a000 00009000
0000000000000080 0000000000000000 WA 0 0 32
[27] .bss NOBITS 000000000000a080 00009080
00000000000001b8 0000000000000000 WA 0 0 32
[28] .gnu_debuglink PROGBITS 0000000000000000 00009080
0000000000000034 0000000000000000 0 0 4
[29] .shstrtab STRTAB 0000000000000000 000090b4
000000000000011d 0000000000000000 0 0 1
系统识别出来的CPU成为逻辑CPU
,开启超线程,每一个线程都会被识别为逻辑CPU
sched.c
taskset通过-c选项,可以指定程序仅运行在指定的逻辑CPU上。
taskset -c 0 ./sched <n> <total> <resol>
是指切换正在逻辑CPU上运行的进程。
运行态(R):正在逻辑CPU上运行
就绪态(R):进程具备运行条件,等待分配CPU时间
睡眠态(S/D):进程不准备运行,除非发生某事件。在此期间不消耗CPU时间
S是指可通过接收信号回到运行态,D主要出现于等待外部存储器的访问时
僵死状态(Z):进程运行结束,正在等待父进程将其回收
如果没有任务在CPU上运行,逻辑CPU会运行一个被称为空闲进程的不执行任何处理的特殊进程
ubuntu@VM-4-14-ubuntu:~$ cat loop.py
while True:
pass
ubuntu@VM-4-14-ubuntu:~$ taskset -c 0 python3 loop.py &
[1] 2818225
ubuntu@VM-4-14-ubuntu:~$ sar -P ALL 1
Linux 5.4.0-42-generic (VM-4-14-ubuntu) 10/31/2022 _x86_64_ (2 CPU)
01:27:20 PM CPU %user %nice %system %iowait %steal %idle
01:27:21 PM all 55.22 0.00 1.00 0.00 0.00 43.78
01:27:21 PM 0 100.00 0.00 0.00 0.00 0.00 0.00
01:27:21 PM 1 10.89 0.00 1.98 0.00 0.00 87.13
01:27:21 PM CPU %user %nice %system %iowait %steal %idle
01:27:22 PM all 50.50 0.00 0.00 0.00 0.00 49.50
01:27:22 PM 0 100.00 0.00 0.00 0.00 0.00 0.00
01:27:22 PM 1 1.00 0.00 0.00 0.00 0.00 99.00
01:27:22 PM CPU %user %nice %system %iowait %steal %idle
01:27:23 PM all 51.01 0.00 0.51 0.00 0.00 48.48
01:27:23 PM 0 100.00 0.00 0.00 0.00 0.00 0.00
01:27:23 PM 1 1.02 0.00 1.02 0.00 0.00 97.96
01:27:23 PM CPU %user %nice %system %iowait %steal %idle
01:27:24 PM all 62.81 0.00 0.50 1.01 0.00 35.68
01:27:24 PM 0 100.00 0.00 0.00 0.00 0.00 0.00
01:27:24 PM 1 25.25 0.00 1.01 2.02 0.00 71.72
吞吐量:单位时间内的总工作量,越大越好
吞吐量 = 处理完成的进程数量 / 耗费的时间
延迟:各种处理从开始到完成所耗费的时间,越短越好
延迟 = 结束处理的时间 - 开始处理的时间
sar 命令中的%idle字段,
ubuntu@VM-4-14-ubuntu:~$ sar -q 1 1
Linux 5.4.0-42-generic (VM-4-14-ubuntu) 10/31/2022 _x86_64_ (2 CPU)
08:24:36 PM runq-sz plist-sz ldavg-1 ldavg-5 ldavg-15 blocked
08:24:37 PM 0 379 0.15 0.15 0.08 0
Average: 0 379 0.15 0.15 0.08 0
sar -q的runq-sz字段。该字段显示的是处于运行态或者就绪态的进程总数(全部逻辑CPU的合计值)
获取cpu数量
$ grep -c processor /proc/cpuinfo
2
ps -eo的命令,etime字段和time字段分别表示进程从开始到执行命令为止的运行时间和执行时间
$ ps -eo pid,comm,etime,time
PID COMMAND ELAPSED TIME
1 systemd 32-07:08:42 00:00:38
2 kthreadd 32-07:08:42 00:00:01
3 rcu_gp 32-07:08:42 00:00:00
4 rcu_par_gp 32-07:08:42 00:00:00
6 kworker/0:0H-kb 32-07:08:42 00:00:00
9 mm_percpu_wq 32-07:08:42 00:00:00
10 ksoftirqd/0 32-07:08:42 00:04:46
11 rcu_sched 32-07:08:42 00:14:16
12 migration/0 32-07:08:42 00:00:15
13 idle_inject/0 32-07:08:42 00:00:00
14 cpuhp/0 32-07:08:42 00:00:00
15 cpuhp/1 32-07:08:42 00:00:00
nice()系统调用,可以设定进程的运行优先级
$ nice -n 5 echo hello
Hello
# sar命令输出的%nice字段表示从默认值0更改为其他优先级后,进程运行时间所占的比例
$ nice -n 5 python3 ./loop.py &
2895557
$ sar -P ALL 1 1
Linux 5.4.0-42-generic (VM-4-14-ubuntu) 10/31/2022 _x86_64_ (2 CPU)
09:31:03 PM CPU %user %nice %system %iowait %steal %idle
09:31:04 PM all 2.00 50.00 0.50 0.00 0.00 47.50
09:31:04 PM 0 0.00 100.00 0.00 0.00 0.00 0.00
09:31:04 PM 1 4.00 0.00 1.00 0.00 0.00 95.00
Average: CPU %user %nice %system %iowait %steal %idle
Average: all 2.00 50.00 0.50 0.00 0.00 47.50
Average: 0 0.00 100.00 0.00 0.00 0.00 0.00
Average: 1 4.00 0.00 1.00 0.00 0.00 95.00
taskset命令也是OS提供的调度器相关的程序,该命令请求被称为sched_setaffinity()的系统调用,将程序限定在指定的逻辑CPU上运行。
$ free
total used free shared buff/cache available
Mem: 4030588 1732532 155376 2828 2142680 1855968
Swap: 0 0 0
# sar -r 可以指定采集周期
# free == kbmemfree
# buff/cache == kbbuffers/kbcached
$ sar -r 1
Linux 5.4.0-42-generic (VM-4-14-ubuntu) 10/31/2022 _x86_64_ (2 CPU)
10:18:44 PM kbmemfree kbavail kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
10:18:45 PM 161236 1863956 1640924 40.71 83088 1458708 3659512 90.79 1570012 625348 3436
10:18:46 PM 162440 1865220 1639636 40.68 83088 1458768 3659512 90.79 1569772 625392 3540
10:18:47 PM 162780 1865560 1639296 40.67 83088 1458768 3659512 90.79 1568956 625388 3540
10:18:48 PM 162008 1864804 1640024 40.69 83088 1458772 3660052 90.81 1569068 625388 3544
10:18:49 PM 161400 1864276 1640556 40.70 83088 1458848 3660052 90.81 1570620 625456 3308
在进入OOM状态,内存管理系统会运行被称为OOM Killer的可怕功能。它会选择合适的进程并将其强制终止,以释放出更多的内存。
sysctl的vm.panic_on_oom默认是0,在发生OOM时运行OOM Killer,
变更为1,在发生OOM时强制关闭系统
虚拟内存使进程无法直接访问系统上搭载的内存,取而代之的是通过虚拟地址间接访问。
进程可以看见的是虚拟地址,系统上搭载的内存的实际地址称为物理地址
readelf
cat /proc/pid/maps输出地址是虚拟地址。
通过保存在内核使用的内存中的页表,可以完成从虚拟页表到物理地址的转换。
在页表中,一个页面对应的数据条目称为页表项。
如果进程访问的地址没有关联物理内存,则在CPU上会发生缺页中断。
缺页中断可以中止正在执行的命令,并启动内核中的缺页中断机构处理,内核中的缺页中断机构检测到非法访问,
向进程发送SIGSEGV信号,接收到该信号的进程通常被强制结束运行。
cat segv.c
#include<stdio.h>
#include<stdlib.h>
int main(void){
int *p = NULL;
puts("before invalid access");
*p = 0;
puts("after invalid access");
exit(EXIT_SUCCESS);
}
$ cc -o segv segv.c
$ ./segv
before invalid access
Segmentation fault (core dumped)
# 由于访问了非法的地址,所以触发了SIGSEGV信号,
读取进程可执行文件,代码段的大小(100)+数据段的大小(200)=300,在物理内存上划分300的区域,将其分配给进程,并把代码和数据复制过去。
Linux的物理内存分配使用的是更复杂的请求分页方法。
在复制完成后,创建进程的页表,并把虚拟地址映射到物理地址。
最后,从指定的地址开始运行即可。
如果进程请求更多内存,内核将为其分配新的内存,创建相应的页表。
C语言标准库中有一个名为malloc()的函数,用于获取内存,在Linux中,这个函数底层调用了mmap()函数。
图5-20待补充
mmap是以页为单位获取内存,malloc是以字节为单位获取内存,为了以字节为单位获取内存,glibc事先通过系统调用mmap向内核申请一大块内存区作为内存池。
当程序调用malloc时,会从池子里划分相应的大小,如果内存池耗尽,glibc会申请mmap申请新的内存空间
显示进程的内存映射信息(/proc/pid/maps)
mmap()函数会通过系统调用向Linux内核请求新的内存。
system()函数会执行第1个参数中指定的命令
mmap.c
使用mmap()函数,将文件的内容读取到内存中,然后将这个内存区域映射到虚拟地址空间。
通过¥memcpy()函数把数据复制到内存映射的区域,同样能更新文件内容
$ echo hello > testfile
$ cat filemap.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <err.h>
#define BUFFER_SIZE 1000
#define ALLOC_SIZE (100*1024*1024)
static char command[BUFFER_SIZE];
static char file_contents[BUFFER_SIZE];
static char overwrite_data[] = "HELLO";
int main(void)
{
pid_t pid;
pid = getpid();
snprintf(command, BUFFER_SIZE, "cat /proc/%d/maps", pid);
puts("*** memory map before mapping file ***");
fflush(stdout);
system(command);
int fd;
fd = open("testfile", O_RDWR);
if (fd == -1)
err(EXIT_FAILURE, "open() failed");
char * file_contents;
file_contents = mmap(NULL, ALLOC_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (file_contents == (void *) -1) {
warn("mmap() failed");
goto close_file;
}
puts("");
printf("*** succeeded to map file: address = %p; size = 0x%x ***\",
file_contents, ALLOC_SIZE);
puts("");
puts("*** memory map after mapping file ***");
fflush(stdout);
system(command);
puts("");
printf("*** file contents before overwrite mapped region: %s", file_contents);
// 覆写映射区域
memcpy(file_contents, overwrite_data, strlen(overwrite_data));
puts("");
printf("*** overwritten mapped region with: %s\", file_contents);
if (munmap(file_contents, ALLOC_SIZE) == -1)
warn("munmap() failed");
close_file:
if (close(fd) == -1)
warn("close() failed");
exit(EXIT_SUCCESS);
}
$ cc -o filemap filemap.c
$ ./filemap
$ cat testfile
HELLO
在请求分页的机制中,对于虚拟地址空间的各个页面,只有在进程初次访问页面时,才会为这个页面分配内存。
页面的状态:未分配进程、已分配给进程且已分配物理内存、已分配给进程尚未分配物理内存
处理流程:
实验:
demand-paging
结论:
ps -eo中的vsz/rss/maj_flt/min_flt分别对应虚拟内存量/已分配物理内存量/创建进程后发生缺页中断的总次数
sar -B 1 的fault/s 表示每秒发生缺页中断次数
在发起fork()系统调用时,并非把父进程的所有内存数据复制给子进程,而是仅仅复制了父进程的页表。
只发生读取操作,父进程与子进程都可以访问共享的物理页面。
当其中一方打算更改任意页面的数据时,则按照下面的流程解除共享。
因为物理内存不是在fork()系统调用时进行复制的,而是在尝试写入时才进行复制的,所以这个机制被称为写时复制(Copy on Write,CoW)
cow.c
将外部存储器的一部分容量暂时当作内存使用。即在系统物理内存不足的情况下,当出现获取物理内存申请时,物理内存的一部分将被保存到外部存储器,从而空出充足的可用内存。这个用于保存页面的区域被称为交换分区。
交换分区有系统管理员在构建系统时进行设置。
换出:在没有空闲的内存,内核会将正在使用的物理内存中的一部分页面保存到交换分区。
换入:内核从交换分区中将先前换出的页面重新拿回到物理内存
换入与换出这两个处理统称为交换。
换入换出也被称为页面调入与页面调出。
需要访问外部存储器的缺页中断叫硬性页缺失
无需访问外部存储器的缺页中断叫软性页缺失
# 查看系统交换分区的信息
$ swapon --show
# swap这一行是交换分区的信息,
$ free
total used free shared buff/cache available
Mem: 4030588 1672344 155496 2816 2202748 1915148
Swap: 0 0 0
# 查看系统中是否发生了交换
$ sar -W 1
Linux 5.4.0-42-generic (VM-4-14-ubuntu) 11/03/2022 _x86_64_ (2 CPU)
09:33:12 PM pswpin/s pswpout/s
09:33:13 PM 0.00 0.00
09:33:14 PM 0.00 0.00
09:33:15 PM 0.00 0.00
# 当发生交换时,如果kbsswpused%一直增加,就非常危险
$ sar -S
x86_64架构的页表采用多级结构
sar -r ALL中的kbpgtbl字段查看页表所使用的物理内存量
问题:
标准大页是比普通的页面更大的页,利用这种页面,能有效减少进程页表所需的内存量
在C语言中,通过mmap()函数的flags参数MAP_HUGETAB标志,可以获取标准大页
当虚拟地址空间连续多个4KB页面符合特定条件时,通过透明大页机制能将它们自动转换成一个大页
$ cat /sys/kernel/mm/transparent_hugepage/enabled
always [madvise] never
# always表示启用
# never表示未启用
# 当设定madvise时,表示仅对有madvise()系统调用设定的内存区域启动透明大页机制
# 启用
$ sudo su
$ echo always > /sys/kernel/mm/transparent_hugepage/enabled
高速缓存抹平寄存器与内存之间的性能差距
高速缓存是把内存上的数据缓存到高速缓存上,
高速缓存以缓存块为单位处理数据
高速缓存分层机构,分为L1 L2 L3
计算机运作流程
从内存往寄存器里读取数据的是欧,数据先被发送到高速缓存,再被送往寄存器
寄存器重新写回到高速缓存上,会有脏标记,这些被标记的数据会在某个指定的时间点,通过后台处理写入内存
高速缓存不足的时候,需要销毁一个现有的缓存块。
构成分层结构的各高速缓存分别名为L1、L2、L3
高速缓存信息可以通过/sys/devices/system/cpu/cpu0/cache/index0
目录下查看
ubuntu@VM-4-14-ubuntu:/sys/devices/system/cpu/cpu0/cache/index1$ ls -l
total 0
-r--r--r-- 1 root root 4096 Oct 25 22:27 coherency_line_size # 缓存块大小
-r--r--r-- 1 root root 4096 Oct 25 22:27 id
-r--r--r-- 1 root root 4096 Oct 25 22:27 level
-r--r--r-- 1 root root 4096 Oct 25 22:27 number_of_sets
-r--r--r-- 1 root root 4096 Oct 25 22:27 physical_line_partition
-r--r--r-- 1 root root 4096 Oct 25 22:27 shared_cpu_list # 共享该缓存的逻辑CPU列表
-r--r--r-- 1 root root 4096 Oct 25 22:27 shared_cpu_map
-r--r--r-- 1 root root 4096 Oct 25 22:26 size # 容量大小
-r--r--r-- 1 root root 4096 Oct 25 22:26 type # Data代表数据,Code代表仅缓存指令模块,Unified代表两者都能缓存
-rw-r--r-- 1 root root 4096 Oct 25 22:27 uevent
-r--r--r-- 1 root root 4096 Oct 25 22:27 ways_of_associativity
cache.c
与CPU访问内存的速度比起来,访问外部存储器的速度慢了几个数量级
内核中用于填补这一速度差的机构称为页面缓存。
页面缓存是把外部存储器上的文件数据缓存到内存上。
页面缓存以页为单位处理数据
内核内存有一个管理区域,缓存了缓存文件的相关信息
当再次读取同样的数据,将直接返回页面缓存中的数据
强制断电会导致页面缓存中的脏页丢失。
可以通过open()调用打开文件时将flag参数设定为O_SYNC,这样执行write()系统调用,都会在页面缓存写入数据时,将数据同步写入外部存储器
# 创建名为testfile的文件,大小为1G
$ dd if=/dev/zero of=testfile oflag=direct bs=1M count=1K
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 10.8637 s, 98.8 MB/s
$ du -h testfile
1.1G testfile
$ free
total used free shared buff/cache available
Mem: 4030588 1666084 411560 2816 1952944 1913192
Swap: 0 0 0
ubuntu@VM-4-14-ubuntu:~$ time cat testfile >/dev/null
real 0m5.536s
user 0m0.007s
sys 0m0.401s
ubuntu@VM-4-14-ubuntu:~$ free
total used free shared buff/cache available
Mem: 4030588 1663684 126148 2816 2240756 1912192
Swap: 0 0 0
# 被缓存住了,
ubuntu@VM-4-14-ubuntu:~$ time cat testfile >/dev/null
real 0m0.153s
user 0m0.000s
sys 0m0.134s
ubuntu@VM-4-14-ubuntu:~$ free
total used free shared buff/cache available
Mem: 4030588 1663404 124228 2816 2242956 1912456
Swap: 0 0 0
# kbcached字段获取页面缓存的总量
$ sar -r 1
Linux 5.4.0-42-generic (VM-4-14-ubuntu) 11/03/2022 _x86_64_ (2 CPU)
10:37:51 PM kbmemfree kbavail kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
10:37:52 PM 1139244 1912708 1581652 39.24 57504 960928 3607924 89.51 1190080 422880 5784
Average: 1139168 1912651 1581724 39.24 57504 960947 3608207 89.52 1190105 422896 5803
ubuntu@VM-4-14-ubuntu:~$ time cat testfile >/dev/null
real 0m6.215s
user 0m0.000s
sys 0m0.446s
ubuntu@VM-4-14-ubuntu:~$ sar -r 1
Linux 5.4.0-42-generic (VM-4-14-ubuntu) 11/03/2022 _x86_64_ (2 CPU)
10:38:09 PM kbmemfree kbavail kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
10:38:10 PM 114656 1913100 1581276 39.23 55016 1988440 3607924 89.51 1185372 1452820 5988
#!/bin/bash
rm -f testfile
echo "$(date): start file creation"
dd if=/dev/zero of=testfile oflag=direct bs=1M count=1K
echo "$(date): end file creation"
echo "$(date): sleep 3 seconds"
sleep 3
echo "$(date): start 1st read"
cat testfile >/dev/null
echo "$(date): end 1st read"
echo "$(date): sleep 3 seconds"
sleep 3
echo "$(date): start 2st read"
cat testfile >/dev/null
echo "$(date): end 2nd read"
rm -f testfile
外部存储器的I/O吞吐量,sar -d -p 1 rd_sec/s 和wr_sec/s分别表示每秒读取数据量和写入数据量,这两个数值以名为扇区的单元为单位
页面调入与页面调出 sar -B 1
#!/bin/bash
rm -f testfile
echo "$(date): start write (file creation)"
dd if=/dev/zero of=testfile bs=1M count=1K
echo "$(date): end write"
rm -f testfile
脏页回写周期可以通过sysctl中的vm.dirty_writeback_centisecs参数更改
$ sysctl vm.dirty_writeback_centisecs
vm.dirty_writeback_centisecs = 500
# 设置为0,回写操作禁止
# 单位里秒(1/100秒)
内存不足时防止产生剧烈的回写负荷
$ sysctl vm.dirty_background_ratio
vm.dirty_background_ratio = 10
# 当脏页占用的内存量与系统搭载的内存总量超过百分比值
当脏页内存占比超过vm.dirty_ratio,将阻塞进程的写入
$ sysctl vm.dirty_ratio
vm.dirty_ratio = 20
清除页面缓存
$ echo 3 > /proc/sys/vm/drop_caches
Linux在访问外部存储器中的数据时,通常不会直接访问,而是通过更加便捷的方式-文件系统来进行访问
为了分门别类地整理文件,Linux的文件系统提供了一种可以收纳其他文件的特殊文件,这种文件称为目录。
通过磁盘配额(quota)功能限制各种用途的文件系统容量
磁盘配额类型:
在外部存储器读写文件系统的数据时被强制切段电源的情况
防止文件系统不一致的技术有很多,常用的是日志与写时复制
ext4与XFS利用的是日志,Btrfs利用的是写时复制
日志功能在文件系统中提供一个名为日志区域的特殊区域。
日志区域是用户无法识别的元数据
如果在更新日志记录的过程中被强制切断电源,那只需丢弃日志区域的数据即可
如果在实际执行数据更新的过程中,被强制切断电源,那只需执行一遍日志记录
ext4和XFS等传统的文件系统上,文件一旦被创建,其位置原则上不会发生变化,即使更新文件内容,也只会在外部存储器的同一位置写入更新的数据。
Btrfs等利用写时复制的文件系统上,创建文件后的每一次更新处理都会把数据写入不同的位置。
如果只创建了文件时,被强制切断电源,再重启后删除新创建的文件
如果创建了文件也进行链接时,被强制切断电源,再重启后删除未处理完的数据
Linux会将自身所处的硬件系统上几乎所有的设备呈现为文件形式,因此在Linux上,设备可以通过open()、read()、write()等系统调用进行访问。
通常情况下,只有root用户可以访问设备文件
以文件形式存在的设备分为两种类型:字符设备与块设备
# 行首字母为c的是字符设备,为b的是块设备
# 第5个字段是主设备号,第6个字段是次设备号
$ ls -l /dev
total 0
crw-r--r-- 1 root root 10, 235 11月 4 14:28 autofs
drwxr-xr-x 2 root root 80 11月 4 22:27 block
drwxr-xr-x 3 root root 60 11月 4 14:27 bus
drwxr-xr-x 2 root root 2640 11月 4 14:28 char
crw------- 1 root root 5, 1 11月 6 12:28 console
write()系统调用:向终端输出数据
read()系统调用:向终端输入数据
$ ps -ef |grep bash
root 5794 5793 0 17:45 pts/0 00:00:02 -bash
root 15970 15969 0 19:45 pts/1 00:00:00 -bash
root 15997 15996 0 19:46 pts/0 00:00:00 bash
root 16016 16015 0 19:48 pts/0 00:00:00 -bash
$ echo "hello" > /dev/pts/0
hello
# 通过指定/dev/pts/1,可以往其他终端写入数据
echo hello > /dev/pts/1
除了能执行普通的读写操作外,还能进行随机访问,比较具有代表性的设备是HDD与SSD等外部存储器
只需像读文件一样读写块设备的数据,即可访问外部存储器中指定的数据。
直接操作块设备
mkfs.ext4 /dev/sdc7 # 创建一个ext4文件系统
mount /dev/sdc7 /mnt/ # 挂载文件系统,
echo "hello world" > /mnt/testfile
ls /mnt/
cat /mnt/testfile
unmount /mnt
strings -t x /dev/sdc7 # 提取字符串信息
echo "HELLO WORLD" > testfile-overwrite
dd if=testfile-overwiret of=/dev/sdc7 seek=$((0x803d000)) bs=1
# 可以通过块设备更改数据
tmpfs是一种创建于内存上的文件系统,切断电源后消失,但是不需要访问外部存储器,所以能提高访问速度
tmpfs通常被用于**/tmp和/var/run**
$ mount |grep ^tmpfs
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
tmpfs on /run type tmpfs (rw,nosuid,nodev,mode=755)
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
tmpfs on /run/netns type tmpfs (rw,nosuid,nodev,mode=755)
tmpfs on /run/user/0 type tmpfs (rw,nosuid,nodev,relatime,size=77536k,mode=700)
挂载时,通过size选项指定最大容量,在初次访问文件系统中的区域时,以页单位申请相应大小的内存。
free中的shared字段表示实际占用的内存量
Windows上的cifs文件系统
Linux上的nfs文件系统
procfs
/proc/cpuinfo CPU的相关信息
/prod/diskstat 外部存储器的相关信息
/proc/meminfo 内存的相关信息
/proc/sys 内核的各种调优参数,与sysctl命令和/etc/sysctl.conf的调优参数一一对应
procfs用于获取系统上所有进程的信息,通常被挂载在/proc目录下
$ ls /proc/$$
arch_status cgroup coredump_filter exe io maps mountstats oom_adj patch_state sched smaps statm timers
attr clear_refs cpuset fd limits mem net oom_score personality schedstat smaps_rollup status timerslack_ns
autogroup cmdline cwd fdinfo loginuid mountinfo ns oom_score_adj projid_map sessionid stack syscall uid_map
auxv comm environ gid_map map_files mounts numa_maps pagemap root setgroups stat task wchan
# /proc/pid/maps: 进程的内存映射
# /proc/pid/cmdline: 进程的命令行映射
# /proc/pid/stat: 进程的状态,比如CPU时间、优先级、内存使用量
sysfs
/sys/devices 设备的相关信息
$ ls -l /sys/devices
total 0
drwxr-xr-x 3 root root 0 Nov 6 20:21 breakpoint
drwxr-xr-x 3 root root 0 Nov 6 20:21 isa
drwxr-xr-x 4 root root 0 Nov 6 20:21 kprobe
drwxr-xr-x 6 root root 0 Nov 6 20:21 LNXSYSTM:00
drwxr-xr-x 5 root root 0 Nov 6 20:21 msr
drwxr-xr-x 16 root root 0 Nov 6 20:13 pci0000:00
drwxr-xr-x 10 root root 0 Nov 6 20:21 platform
drwxr-xr-x 8 root root 0 Nov 6 20:21 pnp0
drwxr-xr-x 3 root root 0 Nov 6 20:21 software
drwxr-xr-x 10 root root 0 Nov 6 20:13 system
drwxr-xr-x 3 root root 0 Nov 6 20:21 tracepoint
drwxr-xr-x 4 root root 0 Nov 6 20:21 uprobe
drwxr-xr-x 16 root root 0 Sep 29 13:54 virtual
/sys/fs 各种文件系统的相关信息
$ ls -l /sys/fs
total 0
drwxr-xr-x 2 root root 0 Nov 6 20:22 aufs
drwx-----T 2 root root 0 Sep 29 13:54 bpf
drwxr-xr-x 3 root root 0 Nov 6 20:22 btrfs
drwxr-xr-x 15 root root 380 Sep 29 13:54 cgroup
drwxr-xr-x 2 root root 0 Nov 6 20:22 ecryptfs
drwxr-xr-x 4 root root 0 Nov 6 20:22 ext4
drwxr-xr-x 3 root root 0 Sep 29 13:54 fuse
drwxr-x--- 2 root root 0 Sep 29 13:54 pstore
cgroupfs
用于限制单个进程或者多个进程组成的群组的资源使用量
通常挂载在**/sys/fs/cgroup**下
cpu:通过读写/sys/fs/cgroup/cpu目录下的文件进行控制
内存:通过读写/sys/fs/cgroup/memory目录下的文件进行控制
HDD用磁性信息表示数据,并将这些数据记录在称为盘片的池畔上,
HDD读写数据的单位时扇区
HDD通过名为磁头的部件读写盘片上各个扇区的数据。
当程序访问了外部存储器上的某个区域后,很有可能继续访问紧跟在后面的区域,
预读机制正是基于这样的推测,预先读取那些接下来可能被访问的区域
在访问SSD上的数据时,不会发生任何机械处理,只需要执行电子处理即可完成访问。