GENIA

linux 基础实验

2023-12-08

小实验记录

阅读过程中发现了一些好玩的实操部分,正好提供了部分源码供下载就试试看。图一乐。

我的 ubuntu20.04 只有双核,多核实验具有局限性。另外虚拟机的实验结果和实机系统有一些微小的差异,不过我觉得可以忽略不计。

系统调用

strace 命令

以 hello.c 为例:

1
2
3
4
5
6
7
#include <stdio.h>

int main(void)
{
puts("hello world");
return 0;
}

strace 命令可以追踪查看程序使用了哪些系统调用:

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
-non-kernel-os$ strace -o hello.log ./hello
hello world
giantbranch@ubuntu:~/Desktop/【实验程序】linux-in-practice-master/02-syscall-and
-non-kernel-os$ cat hello.log
execve("./hello", ["./hello"], [/* 60 vars */]) = 0
brk(NULL) = 0x1875000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=95961, ...}) = 0
mmap(NULL, 95961, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f8d73c61000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`\t\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1868984, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8d73c60000
mmap(NULL, 3971488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f8d7368a000
mprotect(0x7f8d7384a000, 2097152, PROT_NONE) = 0
mmap(0x7f8d73a4a000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000) = 0x7f8d73a4a000
mmap(0x7f8d73a50000, 14752, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f8d73a50000
close(3) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8d73c5f000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8d73c5e000
arch_prctl(ARCH_SET_FS, 0x7f8d73c5f700) = 0
mprotect(0x7f8d73a4a000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ) = 0
mprotect(0x7f8d73c79000, 4096, PROT_READ) = 0
munmap(0x7f8d73c61000, 95961) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 4), ...}) = 0
brk(NULL) = 0x1875000
brk(0x1896000) = 0x1896000
write(1, "hello world\n", 12) = 12
exit_group(0) = ?
+++ exited with 0 +++

每一行对应一个系统调用,大部分都是在main函数之前或之后执行的程序的开始和终止处理发起的(os提供的功能)。

无论使用何种语言编写的程序,都必须通过系统调用向内核发起请求,这一点可以用 python 进行试验:

1
2
3
4
5
6
giantbranch@ubuntu:~/Desktop/【实验程序】linux-in-practice-master/02-syscall-and
-non-kernel-os$ cat hello.py
print("hello world")
giantbranch@ubuntu:~/Desktop/【实验程序】linux-in-practice-master/02-syscall-and
-non-kernel-os$ strace -o hello.py.log python3 ./hello.py
hello world

输出内容比上一次 strace 多得多(应该是python解析器的调用),但是有关 main 函数的主要系统调用还是那么几行。

另外,在 strace 命令后加上 -T 选项能够以很高的精度捕捉到每一种系统调用所消耗的时间, -tt 选项能够显示每一种内核处理发生的时刻。

sar 命令

需要安装 sysstat 软件包:如何在 Ubuntu 20.04 LTS 上安装 SysStat-云东方 (yundongfang.com)

用于获取进程分别在用户模式与内核模式下运行的时间比例。我们指定采集信息的周期与次数(分别为一次):

我的 ubuntu20.4 虚拟机设置了双核心,因此这里的 CPU 核心索引为 0-1。

%user 和 %nice 字段相加为进程在用户模式下运行的时间比例。

%system 则是在内核模式下执行系统调用等处理所占的时间比例。

在这里我没有运行任何程序,CPU 处于完全的空闲状态。

sar 命令还可以监测进程在各模式下的运行时间:

写一个无限循环 loop.c 进行测试

1
2
3
4
5
int main(void)
{
for (;;)
;
}

编译后运行,再用 sar 监测:

可以猜出进程始终通过 CPU-0 运行,且一直在用户模式下运行。单纯的 for 循环不涉及系统调用 0_0

kill 当前进程。

改写一下循环,加入一个函数再进行测试:

1
2
3
4
5
6
7
8
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
for (;;)
getppid();
}

很明显修改后的程序在运行过程中内核处理的时间大大增加。

ldd 命令

可以查看程序所依赖的库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
teriteri@ubuntu:~/Desktop/【实验程序】linux-in-practice-master/02-syscall-and-no
n-kernel-os$ ldd /bin/echo
linux-vdso.so.1 (0x00007fffcd1f1000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff7bf4d9000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff7bf6e8000)
teriteri@ubuntu:~/Desktop/【实验程序】linux-in-practice-master/02-syscall-and-no
n-kernel-os$ ldd /usr/bin/python3
linux-vdso.so.1 (0x00007ffd93136000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdf61e2b000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fdf61e08000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fdf61e02000)
libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007fdf61dfd000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fdf61cae000)
libexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007fdf61c80000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007fdf61c62000)
/lib64/ld-linux-x86-64.so.2 (0x00007fdf6202f000)

可见 python3 本身的实现同样依赖于 libc。

在 OS 层面上,C语言发挥着巨大的作用。

进程管理

fork()

调用 fork() 函数,基于发起调用的进程(父进程),创建一个新的进程(子进程)。

调用流程大概可以这么概括:

  • 为子进程申请内存空间,把父进程的内存复制到新的内存空间。
  • 父进程与子进程分裂为两个进程,分别执行不同代码。因为 fork() 函数返回了不同的值给父进程与子进程。

通过 fork.c 进行实验:

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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <err.h>

static void child()
{
printf("I'm child! my pid is %d.\n", 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.\n",
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()
child();
} else {
// fork() 会返回新创建的子进程的进程 ID(大于 1)给父进程,因此这里调用 parent()
parent(ret);
}
// 在正常运行时,不可能运行到这里
err(EXIT_FAILURE, "shouldn't reach here");
}

两个进程都得到了各自的结果。

execve()

用于启动一个进程,流程大概如下:

  • 读取可执行文件,并读取创建进程的内存映像所需信息(ELF文件内容)
  • 用新进程的数据覆盖当前进程的内存
  • 从最初的命令开始运行新的进程

以 fork-and-exec.c 为例:

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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <err.h>

static void child()
{
char *args[] = { "/bin/echo", "hello" , NULL};
printf("I'm child! my pid is %d.\n", getpid());
fflush(stdout);
execve("/bin/echo", args, NULL);
err(EXIT_FAILURE, "exec() failed");
}

static void parent(pid_t pid_c)
{
printf("I'm parent! my pid is %d and the pid of my child is %d.\n",
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()
child();
} else {
// fork() 会返回新创建的子进程的进程 ID(大于 1)给父进程,因此这里调用 parent()
parent(ret);
}
// 在正常运行时,不可能运行到这里
err(EXIT_FAILURE, "shouldn't reach here");
}

编译运行:

新进程取代了原来 fork() 生成的子进程。

进程调度器

单核心多进程实验

同时运行一个或多个单纯消耗 CPU 计算量的进程,记录

1.不同时间点运行的进程是哪个

2.每个进程的运行进度

源码 sched.c:

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
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <err.h>

#define NLOOP_FOR_ESTIMATION 1000000000UL
#define NSECS_PER_MSEC 1000000UL
#define NSECS_PER_SEC 1000000000UL

static unsigned long nloop_per_resol;
static struct timespec start;

// 计算两个timespec值之间的差值(以纳秒为单位)
static inline long diff_nsec(struct timespec before, struct timespec after)
{
return ((after.tv_sec * NSECS_PER_SEC + after.tv_nsec) - (before.tv_sec * NSECS_PER_SEC + before.tv_nsec));
}

// 估计执行一个毫秒所需的循环迭代次数
static unsigned long estimate_loops_per_msec()
{
struct timespec before, after;
clock_gettime(CLOCK_MONOTONIC, &before);

unsigned long i;
for (i = 0; i < NLOOP_FOR_ESTIMATION; i++)
; // 空循环以消耗一定时间

clock_gettime(CLOCK_MONOTONIC, &after);

int ret;
return NLOOP_FOR_ESTIMATION * NSECS_PER_MSEC / diff_nsec(before, after);
}

// 模拟一些CPU负载,通过执行循环迭代
static inline void load(void)
{
unsigned long i;
for (i = 0; i < nloop_per_resol; i++)
; // 循环迭代以模拟CPU负载
}

// 每个子进程执行的函数
static void child_fn(int id, struct timespec *buf, int nrecord)
{
int i;
for (i = 0; i < nrecord; i++)
{
struct timespec ts;

load(); // 模拟一些CPU负载
clock_gettime(CLOCK_MONOTONIC, &ts); // 记录当前时间
buf[i] = ts;
}
for (i = 0; i < nrecord; i++)
{
// 打印子进程ID、从开始到当前时间的时间差(以毫秒为单位)以及完成百分比
printf("%d\t%ld\t%d\n", id, diff_nsec(start, buf[i]) / NSECS_PER_MSEC, (i + 1) * 100 / nrecord);
}
exit(EXIT_SUCCESS); // 终止子进程
}

static pid_t *pids;

int main(int argc, char *argv[])
{
int ret = EXIT_FAILURE;

if (argc < 4)
{
fprintf(stderr, "usage: %s <nproc> <total[ms]> <resolution[ms]>\n", argv[0]);
exit(EXIT_FAILURE);
}

int nproc = atoi(argv[1]); //同时运行的程序数量 1、2、4
int total = atoi(argv[2]); //程序运行的总时长 100、100、100
int resol = atoi(argv[3]); //采集统计信息的间隔 1、1、1

if (nproc < 1)
{
fprintf(stderr, "<nproc>(%d) should be >= 1\n", nproc);
exit(EXIT_FAILURE);
}

if (total < 1)
{
fprintf(stderr, "<total>(%d) should be >= 1\n", total);
exit(EXIT_FAILURE);
}

if (resol < 1)
{
fprintf(stderr, "<resol>(%d) should be >= 1\n", resol);
exit(EXIT_FAILURE);
}

if (total % resol)
{
fprintf(stderr, "<total>(%d) should be multiple of <resolution>(%d)\n", total, resol);
exit(EXIT_FAILURE);
}
int nrecord = total / resol; // 每个进程要创建的记录数

struct timespec *logbuf = malloc(nrecord * sizeof(struct timespec));
if (!logbuf)
err(EXIT_FAILURE, "failed to allocate log buffer");

puts("estimating the workload which takes just one milli-second...");
nloop_per_resol = estimate_loops_per_msec() * resol; // 计算每个resolution所需的循环次数
puts("end estimation");
fflush(stdout);

pids = malloc(nproc * sizeof(pid_t));
if (pids == NULL)
err(EXIT_FAILURE, "failed to allocate pid table");

clock_gettime(CLOCK_MONOTONIC, &start); // 记录开始时间

ret = EXIT_SUCCESS;
int i, ncreated;
for (i = 0, ncreated = 0; i < nproc; i++, ncreated++)
{
pids[i] = fork(); // 创建子进程
if (pids[i] < 0)
{
int j;
for (j = 0; j < ncreated; j++)
kill(pids[j], SIGKILL); // 如果fork失败,kill创建的所有子进程
ret = EXIT_FAILURE;
break;
}
else if (pids[i] == 0)
{
// 子进程
child_fn(i, logbuf, nrecord); // 执行子进程函数
/* 不应该运行到这里 */
abort(); // 这应该永远不会被执行;在出现错误时中止
}
}
// 父进程
for (i = 0; i < ncreated; i++)
{
if (wait(NULL) < 0)
warn("wait() failed."); // 等待所有子进程完成
}

exit(ret); // 以适当的退出状态终止主进程
}

使用 OS 提供的 taskset 命令可以使程序在指定的逻辑 CPU 上运行,便于观察实验结果。同时将每次输出保存在另外的文件中。

1
$ taskset -c 0 ./sched 参数1 参数2 参数3 >输出文件名.txt

可以自己用 matplotlib 为每组数据分别做两张点阵图,一张是在单核 CPU 上运行的进程(x:开始运行后经过的时间,y:进程 ID),一张表示各进程的进度(x:开始运行后经过的时间,y:进度)。

单核单进程

单核双进程

单核四进程

一些总结

  1. 当只有一核 CPU 工作时,不管同时运行多少个进程,在任意一个时间点上只能有一个进程运行
  2. 当一核 CPU 需要运行多个进程时,它们将按轮询调度的方式循环运行,所有进程按顺序逐个运行,不断循环直至所有进程结束
  3. 每个进程被分配到的时间片的长度大致上是相等的
  4. 全部进程运行结束消耗的时间随着进程数量的增加而等比例地增加

图形实现

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
import os
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] # 中文字体
# 数据处理和分组
def parse_data(filename):
with open(filename, 'r') as f:
data = f.readlines()
process_data = {}
for line in data[2:]:
pid, time_passed, progress = map(int, line.strip().split('\t'))
if pid not in process_data:
process_data[pid] = {'time': [], 'progress': []}
process_data[pid]['time'].append(time_passed)
process_data[pid]['progress'].append(progress)

return process_data
# 图一作图
def plot_time_vs_process(process_data, filename):
plt.figure(figsize=(10, 5)) # 设置图的大小
for pid, data in process_data.items():
plt.scatter(data['time'], [pid] * len(data['time']), label=f'Process {pid}', marker='o', s=20)

plt.xlabel('时间 (ms)')
plt.ylabel('进程 ID')
plt.title('在单核 CPU 上运行的进程')
plt.legend()
plt.xlim(0) # 将横轴的起始值设为0
plt.yticks(range(len(process_data))) # 设置纵坐标的刻度为 0 到最大进程数
plt.savefig(filename + '_1.png') # 保存图片
plt.close()
# 图二作图
def plot_progress(process_data, filename):
plt.figure(figsize=(12, 6)) # 设置图的大小
for pid, data in process_data.items():
plt.scatter(data['time'], data['progress'], label=f'Process {pid}', marker='o', s=14)

plt.xlabel('时间 (ms)')
plt.ylabel('进度 (%)')
plt.title('各进程的进度')
plt.legend()
plt.xlim(0) # 将横轴的起始值设为0
plt.yticks(range(0, 101, 10)) # 设置纵坐标的刻度为 0 到 100,步长为 10
plt.savefig(filename + '_2.png') # 保存图片
plt.close()

if __name__ == '__main__':
file_prefixes = ['1core-1process', '1core-2process', '1core-4process'] # 三个数据文件的前缀名
for prefix in file_prefixes:
filename = prefix + '.txt' # 请将文件名替换为实际的数据文件名
process_data = parse_data(filename)

plot_time_vs_process(process_data, filename)
plot_progress(process_data, filename)

进程状态查看

进程一般有四种状态:

  1. 运行态
  2. 就绪态 ——> 具备运行条件,等待 CPU 分配时间
  3. 睡眠态 ——> 不准备运行,除非发生某些时间,不占用 CPU 时间
  4. 僵死状态 ——> 运行结束,等待父进程回收

ps ax 查看进程状态。

STAT一栏一般有三种状态:

首字母 状态
R 运行态或就绪态
S 或 D 睡眠态
Z 僵死态

S 可通过接收信号回到运行态,D 指 S 以外的情况(主要出现于等待外部存储器地访问时)

一般处于睡眠态的进程所等待的事件有几种:

  • 被要求等待指定的时间(比如 sleep 函数)
  • 等待用户通过键盘或鼠标等设备输入
  • 等待 SDD 或 HDD 等外部存储器的读写结束
  • 等待网络的数据收发结束
1
2
teriteri@ubuntu:~/Desktop$ ps ax | wc -l
281

也可以直接查看进程总数。