cd ~{,/zhs/}
杭电操作系统实验一,个人笔记。
写给 Linux 命令行新人的一点人生经验:
whatis ls
会告诉你 ls 是 list directory contents;man cd
)或者帮助(如 cd --help
),本文中所有选项都已展开成全称。建议使用带有快照功能的虚拟化平台,发行版采用 Arch Linux 是因为有官方文档可以参考。
创建虚拟机时其他选项建议跟随默认,主要建议以下几点:
安装 Arch Linux 请认真参阅官方文档的安装教程,以下只列出几点虚拟机中需要注意的:
cfdisk
工具中选择 dos
类型的分区表类型;最近 archiso 新加入了基于 Reflector 自动排序镜像的功能,不过我觉得还是手动设置一下比较稳妥。
国内镜像源里面清华、中科大知名度都太高了,负载高反倒体验差;建议使用北外源,也是清华提供的技术支持。
# cd /etc/pacman.d/
# mv --verbose mirrorlist{,.backup}
# host='https://mirrors.bfsu.edu.cn'
# echo "Server = $host/archlinux/\$repo/os/\$arch" > mirrorlist
# cat mirrorlist
检查 mirrorlist
文件内容已被修改成 Server = https://mirrors.bfsu.edu.cn/archlinux/$repo/os/$arch
即可。
# pacstrap /mnt base linux-lts grub dhcpcd
base
是 Arch Linux 必需的最基础包组;linux-lts
是 Arch Linux 官方的长期支持版 Linux 内核,落笔时的版本是 5.4.73;grub
是启动加载器,安装完成后一定不要忘记生成配置文件,别问我怎么记住的;dhcpcd
是 DHCP 客户端,重启进入安装好的系统后执行 systemctl enable --now dhcpcd
即可永久开启虚拟机内的网络功能;# pacman --sync wget progress nano
这三个软件包分别是后续步骤中下载、查看解压进度、编辑源码用到的工具。
# pacman --sync base-devel xmlto kmod inetutils bc libelf git
具体构建依赖应以 ArchWiki 为准:Kernels/Traditional compilation - ArchWiki
准备工作不需要也不建议使用 root 权限操作,创建并切换至非特权用户,此处的 liolok
是我的 ID。
# useradd --create-home liolok
# passwd liolok
# su --login liolok
创建一个目录,存放内核源码,并进行编译及构建。
笔者使用的内核构建目录是 ~/kernel-build
,其中「~」表示上一步骤创建的用户主目录 /home/liolok
。
$ mkdir ~/kernel-build && cd ~/kernel-build # make and change to build directory
为什么不用 /usr/src/linux
:
/usr/src
这个目录确实应用来存放各种源码,但应仅用作参考(reference),而不应进行构建。
(参见 Linux 文件系统层次结构标准)
Linux 内核官网:The Linux Kernel Archives
届时最新的 LTS 版本号是 5.4.74(2020.11.01),为了下载得更快推荐使用国内的镜像源(同上文,采用北外源):
$ version='5.4.74'
$ host='https://mirrors.bfsu.edu.cn'
内核源码压缩包的文件格式是 .tar.xz
(也有 .tar.gz
),tar 是打包/归档工具,xz/gz 是压缩工具,至于 .tar.sign
文件是用于验证的签名。
使用 wget
进行下载和解压的方法:
$ pwd # you should be working under `~/kernael-build`
$ wget $host/kernel/v5.x/linux-$version.tar.{xz,sign}
$ unxz linux-$version.tar.xz & progress --monitor --pid $!
完成后构建目录下应该有解压之后的 .tar
文件和 .tar.sign
签名。
本步骤参考 Linux 内核官方文档:Using GnuPG to verify kernel signatures。
下载并导入两位内核主要作者的公匙,验证源码的完整性。如果下载到的文件本身就是损坏的,那编译成功也有可能工作不正常。
$ gpg --locate-keys [email protected] [email protected] # download and import keys
$ gpg --verify linux-$version.tar.sign # verify the .tar archive against the signature
不用管有没有 warning,输出中有「Good signature」字样即可,否则说明源码归档包已损坏。
此时建议为虚拟机创建一个快照,后面即将开始折腾。
现在只需将其解包到同名子目录即可得到内核的源代码:
$ tar --extract --file=linux-$version.tar & progress --monitor --pid $!
$ cd linux-$version
这个步骤我们得到了内核源码的未修改版,也可以做个快照。
每次编译前,都要确保目录下的编译历史已被清理干净,同样也不要指望刚解包得到的源码目录就一定是干净的。
$ make mrproper # ensure the kernel tree is absolutely clean
这里修改 local version 是为内核版本号追加一个后缀,虽然不是必需的,但也方便后续验证编译成果。
$ zcat /proc/config.gz > .config # use Arch Linux default configuration
$ make nconfig # append your custom local version string
输入自定义的版本后缀后,按 F6 保存配置、F9 退出。
使用 time
命令可为编译计时;使用 --jobs=X
选项可并行工作以加快编译,其中 X
为处理器线程数,对应虚拟机的配置。
$ time make --jobs=8 # compile kernel
注意!接下来的步骤均需要以 root 身份进行操作,使用 su
切换至 root 用户继续后续操作。
# make modules_install # compile kernel modules
# cp --verbose ./arch/x86_64/boot/bzImage /boot/vmlinuz-linux-custom # 64-bit kernel
此处的 bzImage
和 vmlinuz-linux-custom
是编译内核及模块得到的压缩可执行文件;命名是约定俗成的,可以自行修改后缀如 vmlinuz-linux-blabla
,后续步骤中还会用到。
拷贝一份已有的 mkinitcpio preset
并加以修改,便可用于生成对应内核的 initramfs 镜像。
# cp --verbose /etc/mkinitcpio.d/linux-lts.preset /etc/mkinitcpio.d/linux-custom.preset # copy a preset
此处如果提示找不到 linux-lts.preset
,可使用 ls /etc/mkinitcpio.d
查看实际文件名称。
# nano /etc/mkinitcpio.d/linux-custom.preset # modify copied preset
进行如下修改:
...
ALL_kver="/boot/vmlinuz-linux-custom"
...
default_image="/boot/initramfs-linux-custom.img"
...
fallback_image="/boot/initramfs-linux-custom-fallback.img"
最后,为内核生成 initramfs 镜像。
# mkinitcpio --preset linux-custom # generate initramfs images
# cp --verbose ./System.map /boot/System.map-linux-custom
# ln --verbose --symbolic --force /boot/System.map-linux-custom /boot/System.map # create a symlink
至此,使用 ls /boot
检查,应已新增以下文件:
让 GRUB 自动识别新安装到 /boot 的内核并将其添加至引导菜单:
# grub-mkconfig --output=/boot/grub/grub.cfg # update grub configuration
重启后在 GRUB 引导菜单的高级选项中选择新编译安装好的内核:
进入系统后使用 uname --release
命令可以看到之前自定义的版本号后缀,说明已切换至新内核:
Adding a New System Call — The Linux Kernel documentation
在刚刚编译的官方内核的基础上,添加一个自定义的系统调用 setnice
,功能如下:
为了添加系统调用,要对此前的官方内核源码进行微量的修改:
$ cd ~/kernel-build/linux-$version # kernel source code directory
绝大多数情况下,内核会透明地处理指针,从而向旧兼容 32 位的用户空间程序。 对于我们要添加的简单系统调用而言,不需要另外单独实现 32 位兼容版本。
系统调用的函数原型位于内核源码目录下的 ./include/linux/syscalls.h
:
通用系统调用表位于 ./include/uapi/asm-generic/unistd.h
,
按对应格式在列表末尾追加表项(注意也要相应的增加 __NR_syscalls
的值):
Adding a New System Call — The Linux Kernel documentation
x86 架构的系统调用表位于 ./arch/x86/entry/syscalls/syscall_{64,32}.tbl
两个文件中,按对应格式在列表末尾追加表项:
(common
代表 64 位版本的系统调用向旧兼容 32 位用户空间程序;如前文所说,我们自定义的简单系统调用可以通过内核的透明处理自动兼容。)
系统调用的入口点通常位于 ./kernel/sys.c
文件中:
/*
* Set/get nice value and get priority value of a process with given ID;
* Set new nice value only if parameter set is true;
* Get values of nice and priority of the process at last.
*/
SYSCALL_DEFINE5(setnice, pid_t, pid, bool, set, int, niceval,
int __user *, nice, int __user *, prio)
{
int tmp;
struct task_struct *tsk = find_get_task_by_vpid(pid);
if (tsk == NULL)
return -ESRCH; // no such process
if (set == true) // set new nice value
set_user_nice(tsk, niceval);
tmp = task_nice(tsk); // get nice value
if (copy_to_user(nice, &tmp, sizeof(tmp)))
return -EFAULT; // bad address
tmp = task_prio(tsk); // get priority value
if (copy_to_user(prio, &tmp, sizeof(tmp)))
return -EFAULT; // bad address
return 0;
}
宏定义 SYSCALL_DEFINE5()
:
如上图中的 SYSCALL_DEFINE1(sysinfo, struct sysinfo __user *, info)
声明了名为 sysinfo
的系统调用,
其第一个也是唯一的参数是类型为 struct sysinfo __user *
的 info
。
而我们添加的系统调用 setnice
有五个参数:
pid_t
(int
的一个别名)类型的 pid
(number):进程 ID 数字;bool
类型的 set
:为真则修改进程的 nice 数值int
类型的 niceval
:用于修改进程 nice 数值;int __user *
类型的 nice
:用于读取进程的 nice 数值;int __user *
类型的 prio
:用于读取进程的优先级数值。宏定义 __user
是用于内核静态分析的用户空间地址的注解,表示该指针不可直接解引用(dereference)。可以看到后面我们会用到 copy_to_user()
函数来安全地操作。(参见 StackOverflow 上的这个回答)
函数 find_get_task_by_vpid()
:在当前 PID 命名空间中查找 PID 并得到其对应的 PCB。整个过程是一个临界区,使用了 RCU (Read-Copy-Update) 机制 来实现对 PID 命名空间中 PID 基数树的保护。
函数 set_user_nice()
粗略分析:
void set_user_nice(struct task_struct *p, long nice)
{
bool queued, running; // 进程是否在运行队列中、进程是否正在运行
int old_prio, delta; // 原优先级、变化量
struct rq_flags rf;
struct rq *rq;
// 新的 nice 值与原值相同或者超出范围则直接返回
if (task_nice(p) == nice || nice < MIN_NICE || nice > MAX_NICE)
return;
/*
* We have to be careful, if called from sys_setpriority(),
* the task might be in the middle of scheduling on another CPU.
*/
rq = task_rq_lock(p, &rf); // 获取进程所在的运行队列,并使用自旋锁保护运行队列以及进程的优先级继承数据结构
update_rq_clock(rq); // 更新运行队列时钟?
/*
* The RT priorities are set via sched_setscheduler(), but we still
* allow the 'normal' nice value to be set - but as expected
* it wont have any effect on scheduling until the task is
* SCHED_DEADLINE, SCHED_FIFO or SCHED_RR:
*/
if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
p->static_prio = NICE_TO_PRIO(nice);
goto out_unlock;
}
queued = task_on_rq_queued(p); // (p->on_rq == TASK_ON_RQ_QUEUED)
running = task_current(rq, p); // (rq->curr == p)
if (queued)
dequeue_task(rq, p, DEQUEUE_SAVE | DEQUEUE_NOCLOCK); // 「伪」出列,尽量保留状态,不更新时钟
if (running)
put_prev_task(rq, p); // 暂停运行
p->static_prio = NICE_TO_PRIO(nice); // ((nice) + DEFAULT_PRIO) == (120 + nice)
set_load_weight(p, true);
old_prio = p->prio;
p->prio = effective_prio(p); // 对于常规进程,本质上是将前面算好的 static prio 赋值给 prio
delta = p->prio - old_prio; // 计算优先级变化量
if (queued) {
enqueue_task(rq, p, ENQUEUE_RESTORE | ENQUEUE_NOCLOCK); // 修改完毕,「伪」入列
/*
* If the task increased its priority or is running and
* lowered its priority, then reschedule its CPU:
*/
if (delta < 0 || (delta > 0 && task_running(rq, p))) // ?task_running() v.s. task_current()
resched_curr(rq); // 重新调度当前运行队列
}
if (running)
set_next_task(rq, p); // 继续运行
out_unlock:
task_rq_unlock(rq, p, &rf);
}
分析优先级的头文件以及函数 task_nice()
和 task_prio()
可以总结出,对于常规进程而言:
static_prio
、normal_prio
、prio
三者是相同的;MAX_RT_PRIO
即 100,最终范围是 [0, 39]。添加了系统调用的少量代码后,重新编译内核从而运行该系统调用;具体步骤应从清理编译历史开始。
编写一个用户态程序以测试新添加的系统调用,如笔者的 test_setnice.c
代码如下:
#include <sys/syscall.h> // syscall()
#include <unistd.h> // pid_t, getpid()
#include <stdio.h> // scanf(), printf()
#define __NR_setnice 436 // syscall name and number
int main(void)
{
printf("[ Test program for syscall setnice ]\n");
printf("-------------------------------------------\n");
printf("Will test on this process itself by default\n");
printf("or anohter process (need input process id).\n");
printf("-------------------------------------------\n");
printf(":: Test on this process itself? [y/n] ");
pid_t pid; // pid number
char choice;
scanf("%c%*c", &choice); // choose process to test
if (choice != 'y') {
printf(":: Process ID: ");
scanf("%d", &pid);
}
else {
pid = getpid();
printf("Process ID: %d\n", pid);
}
choice = 'g'; // get nice value at first time
printf("Original values:\n");
int count = 1;
do {
int err, niceval, nice, prio;
int set = (choice != 'g');
if (set) {
printf(":: New nice [-20, 19]: ");
scanf("%d%*c", &niceval);
}
if (err = syscall(__NR_setnice, pid, set, niceval, &nice, &prio)) {
printf("Error %d, test terminated.\n", err);
return err;
}
printf("Nice: %d\n", nice);
printf("Priority: %d\n", prio);
printf(":: [%d] Set/get nice value? (Ctrl+C to exit) [s/g] ", count++);
} while (scanf("%c%*c", &choice) != EOF);
}
在终端下编译运行并进行测试,结果如下: