目录
一、实验思路
二、准备工作——Linux内核编译步骤
1.实验环境配置
2.下载内核源码
3.解压缩内核源码文件
4.清除残留的.config和.o文件
5.配置内核
6.编译内核,生成启动映像文件
7.编译模块
8.安装内核
9.配置grub引导程序
10.重启系统
三、设计系统调用,返回指定进程的相关时间信息
1.分配系统调用号,修改系统调用表
2.申请系统调用服务例程原型
3.实现系统调用服务例程
4.重新编译内核
5.编写用户态程序测试新系统调用
四、函数详解
(1)find_get_pid(pid)
(2)pid_task()
(3)copy_to_user()
(4)list_for_each_entry(pos, head, member)
五、关于验收
一、实验思路
题目:返回指定进程的相关时间信息,如进程创建时间、进程在用户态及内核态的运行时间、进程的所有子孙进程在用户态的运行时间及在内核态的运行时间等。
本实验要求设计系统调用,而系统调用的实质是调用内核函数,于内核态中运行。 所以要先编译好内核,再来进行系统调用:
图2.1 实验流程
编译内核的步骤:
图2.2 编译内核步骤
二、准备工作——Linux内核编译步骤
1.实验环境配置
2.下载内核源码
点击进入Linux官方网站下载
3.解压缩内核源码文件
1.将下载的新内核压缩文件复制到/home中
2.进入压缩文件所在子目录
cd /home
3.分两步解压缩:
xz -d linux-6.0.8.tar.xz
(别急,等着它运行完)
tar -xvf linux-6.0.8.tar
运行完啦:
文章来源地址https://www.toymoban.com/news/detail-770800.html
文章来源:https://www.toymoban.com/news/detail-770800.html
4.清除残留的.config和.o文件
注:一定要先进入linux-6.0.8子目录哟
不然会报错:
1.安装ncurses包命令(Ubuntu中为:libncurses5-dev):
apt-get install libncurses5-dev
2.在开始完全重新编译之前,执行如下命令来清除残留的.config和.o文件:
make mrproper
5.配置内核
运行命令:
make menuconfig
解决方法——依次执行如下命令:
apt-get install flex
apt-get install bison
Do you want to continue? y
然后再执行:make menuconfig
Save-Enter-OK-Enter-Enter,最后选择Exit,按下Enter键。
6.编译内核,生成启动映像文件
执行命令:
make -j32
来,让我们看看报了什么错:
不慌,执行下面两条命令安装缺少的包:
apt-get install libelf-dev
apt-get install libssl–dev
缺少bison:
apt-get install bison
缺少flex:
apt-get install flex
还是报错:
scripts/sign-file.c:25:10: fatal error: openssl/opensslv.h: No such file or directory 25 | #include <openssl/opensslv.h> | ^~~~~~~~~~~~~~~~~~~~ compilation terminated.
make[1]: *** [scripts/Makefile.host:95: scripts/sign-file]
Error 1 make: *** [Makefile:1189: scripts]
Error 2 make: *** Waiting for unfinished jobs....
root@xxx:/home/linux-6.0.8# apt-get install libssl–dev Reading package lists... Done Building dependency tree
Reading state information... Done
E: Unable to locate package libssl–dev
解决方法:,先执行apt-get update,再执行apt-get install libssl-dev
libssl-dev安装步骤
Update the package index:
apt-get update
Install libssl-dev deb package:
apt-get install libssl-dev
我用的是make -j16,想快点就make -j32,结果:
7.编译模块
make modules
好哇,又报错:
make[1]**No rule to nake target ‘debian/canonical-certs.pew’, needed by ‘certs/x509_certificate_list’.Stop.
解决方法:
编辑.config文件,输入命令:
vim .config
打开文件后输入“/”查找其所在的命令行(找到之后删除/xxxx,然后按I(Insert)键就不用我说了吧),如下图:
将文件中的CONFIG_SYSTEM_TRUSTED_KEYS置空,如果CONFIG_SYSTEM_REVOCATION_KEYS
的值不为空的话,也将其赋空值。
之后再make -j32
CONFIG_SYSTEM_TRUSTED_KEYS=""
CONFIG_SYSTEM_REVOCATION_KEYS=""
然后再,make -j16,嗯,又报错了,我真是把错都遇到了个遍,挺好:
解决方法——输入以下指令:,再 make -j16
apt-get install dwarves
成功啦!😀
8.安装内核
(1)安装模块:
make modules_install
(2)安装内核:
make install
真碰巧,又报错了,都快做完了,结果/boot空间不够,我。。。
尝试了删除旧的内核版本:apt purge linux-image-5.4.0-26-generic,得到更大的错误:
怎么解决呢,只搜到了这一个答案,喜提再来一遍。。。不哭不哭,继续加油,再来一遍~
再来一遍后成功了(第二遍顺利多了):
9.配置grub引导程序
执行命令:
update-grub2
10.重启系统
执行命令:
reboot
可以使用命令"uname -a"查看内核版本
看到别的博主都把root@主机名马赛克啦,我也意识到这个隐私问题,觉得打马赛克太麻烦啦,于是用这个命令改了1下主机名,姑且叫test吧:
hostname test
三、设计系统调用,返回指定进程的相关时间信息
1.分配系统调用号,修改系统调用表
root@test:~# cd /root
root@test:~# cd linux-6.0.8
root@test:~/linux-6.0.8# vim arch/x86/entry/syscalls/syscall_64.tbl
为新调用添加一条目录:
2.申请系统调用服务例程原型
root@test:~/linux-6.0.8# vim include/linux/syscalls.h
在文件末尾添加以下内容:
asmlinkage long sys_jzc(pid_t pid,u64 __user *start_time,u64 __user *utime,u64 __user *stime,int __user *num);
3.实现系统调用服务例程
root@test:~/linux-6.0.8# vim kernel/sys.c
在sys.c文件中添加下列服务例程:
//该系统调用需要五个参数,mygettime是系统调用名称
SYSCALL_DEFINE5(mygettime,pid_t,pid,u64 __user *,start_time,u64 __user *,utime,u64 __user *,stime,int __user *,num)
{
u64 c_start_time,c_utime,c_stime;
int c_num=0;
struct pid *ppid;
struct task_struct *p;
struct task_struct *pp;
ppid = find_get_pid(pid);
p = pid_task(ppid,PIDTYPE_PID);
c_start_time = p -> start_time;
c_utime = p -> utime;
c_stime = p -> stime;
copy_to_user(start_time,&c_start_time,sizeof(c_start_time));
copy_to_user(utime,&c_utime,sizeof(c_utime));
copy_to_user(stime,&c_stime,sizeof(c_stime));
list_for_each_entry(pp,&(p -> children),sibling)
{
c_num ++;
c_start_time = pp -> start_time;
c_utime = pp -> utime;
c_stime = pp -> stime;
copy_to_user(start_time,&c_start_time,sizeof(c_start_time));
copy_to_user(utime+1,&c_utime,sizeof(c_utime));
copy_to_user(stime+1,&c_stime,sizeof(c_stime));
}
copy_to_user(num,&c_num,sizeof(c_num));
return 0;
}
4.重新编译内核
在第一大部分linux内核编译中,从 4.清除残留的.config和.o文件 再来一遍
make menuconfig //配置内核
make -j16 //编译内核
make modules //编译模块
make modules_install //安装模块
make install // 安装内核
reboot // 立即重启
5.编写用户态程序测试新系统调用
mkdir work2
vim work2.c
work2.c文件:
//可以通过终端命令行pstree -p来查看进程号并选择想选择的pid
#define _GNU_SOURCE
#include<stdio.h>
#include<unistd.h>
#include<sys/syscall.h>
int main()
{
unsigned long long start_time[50],utime[50],stime[50];
int num;
syscall(323,1309,start_time,utime,stime,&num);
printf("num:%d\n",num);
int i;
for(i = 0;i<=num;i++)
{
printf("i:%d,start_time:%llu,utime:%llu,stime:%llu\n",i,start_time[i],utime[i],stime[i]);
}
return 0;
}
root@test:~# cd ..
root@test:/# cd root
root@test:~# gcc work2.c
root@test:~# gcc work2.c -o a
root@test:~# ./a
结果:
四、函数详解
(1)find_get_pid(pid)
位置:kernel/pid.c
源码:
图4.1.1 find_get_pid()源码
功能 :此函数根据提供的进程号nr获取对应的进程描述符,并且将该进程描述符中的字段count加1.即此进程的用户数目加1(在新进程创建之初,进程描述符字段count的值为1,而函数find_get_pid()执行后,进程描述符字段count变为2)
rcu_read_lock()与rcu_read_ulock():
rcu_read_lock()和rcu_read_ulock()是 RCU “随意读” 的关键,它们的效果是声明了一个读端的临界区,用来保持一个读者的RCU临界区.在该临界区内不允许发生上下文切换。
进程上下文指进程切换时需要保持的进程状态,包括寄存器值、用户和核心栈状态。
get_pid:使count字段加1
find_vpid(nr) :通过它来找到进程描述符,在/kernel/pid.c中定义
图4.1.2 find_vpid()源码
图4.1.2 find_vpid()源码解析
图4.1.3 task_active_pid_ns ()源码
task_active_pid_ns ():
这个函数分为两步,第一步通过task_pid 拿到当前task的pid,这里的pid是全局的。
在以pid 为形参通过ns_of_pid 得到pid 对应的namespace
(2)pid_task()
图4.1.2 pid_task()源码
功能:通过pid和pid的类型来获取它的task_struct结构体,也就是PCB,PCB里有我们所需要的进程的相关时间信息。
(3)copy_to_user()
copy_to_user()
图4.1.2 copy_to_user()源码
完成数据从内核空间到用户空间的复制,to 目标地址,这个地址是用户空间的地址;from 源地址,这个地址是内核空间的地址; n将要拷贝的数据的字节数。
如果数据拷贝成功,则返回零;否则,返回没有拷贝成功的数据字节数。
access_ok():作用是检查用户空间指针是否可用。可用则返回True
__copy_to_user():复制功能的具体实现,是用汇编语言实现的
感兴趣的看参考博客13
(4)list_for_each_entry(pos, head, member)
它实际上是一个 for 循环,利用传入的 pos 作为循环变量,从表头 head 开始,逐项向后(next 方向)移动 pos,直至又回head。
作用:第一个member代表head,list_for_each_entry的作用就是循环遍历每一个pos中的member子项,简单来说就是,遍历链表。
但问题的关键在于如何通过pos获得(节点)结构体的起始地址,以便于引用节点的其他域?
这里我们用到了container_of(ptr, type, member)。
list_for_each_entry()里面涉及到的小函数很多,我们先分解一下它:
图4.1.3 list_for_each_entry()源码
图4.1.4 list_first_entry()源码
图4.1.5 list_entry()源码
其中:
图4.2.6 list_entry()宏
而整个宏定义的关键就在于 container_of上面。而这个宏定义如下:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
container_of(ptr, type, member)的做用是:根据一个结构体变量中的一个域成员变量的指针来获取指向整个结构体变量的指针。
可分为两步来看它:
第一步,首先定义一个临时的数据类型(通过typeof(((type *)0)->member)获得)与ptr相同的指针变量__mptr,然后用它来保存ptr的值。
第二步,用(char *)__mptr减去member在结构体中的偏移量,得到的值就是整个结构体变量的首地址(整个宏的返回值就是这个首地址)。
偏移量又是怎么计算的呢?
offsetof(type,member):用来计算一个struct结构体中某个成员相对于结构体首地址的偏移量。
五、关于验收
我是周旭老师班级的,周旭老师讲课超好哦,下面是大家最关心的验收问题:
好像是这样的:我一去验收,老师就说,讲讲吧,你用到了哪些函数啊,然后我就开始讲上面的第四部分的函数详解,期间问了我几个问题,我很肯定地回答了他,后来问我
1.task_struct
结构体在哪定义的,我知道他上课说过,看linux代码要从task_struct结构体看起,但我找了别的就没找这个,哎栽了:task_struct
结构体在 /include/linux/sched.h中定义 ;2.好像还问了我copy_to_user()的作用
把数据从内核空间复制到用户空间。
3.ptr减去偏移量就是结构体type类型所对应的地址具体是怎么做的,懂不懂?
当即开始胡言乱语了,还自己问自己:这个偏移量是怎么求的,真是自己给自己找事,(offsetof(type,member) 是求偏移量的),具体看上面讲的:list_for_each_entry(pos, head, member)
大抵就是这些了,若有错麻烦大家多多指正、多多包涵,谢谢!
参考博客:
1.编译Linux内核kernel时遇到的问题与解决方案
2.内核错误:BTF: .tmp_vmlinux.btf: pahole (pahole) is not available
3.内核错误: No rule to make target ‘debian/canonical-certs.pem‘, needed by ‘certs/x509_certificate_list‘
4.libssl-dev安装步骤
5.杭电(杭州电子科技大学)操作系统实验一:Linux内核编译及添加系统调用(返回指定进程的相关时间信息)
6.Linux源码
7.find_get_pid 解析
8.进程管理API之find_get_pid
9.进程管理API之task_active_pid_zhong
10.Linux 内核学习知识:浅析 offsetof 宏以及内核开发学习的所思所想(内核开发人员必读)
11.Linux内核中的container_of函数简要介绍
12.list_for_each_entry(pos, head, member)的内幕
13.初步解析内核函数copy_to_user和copy_from_user
到了这里,关于HDU 操作系统实验二 -设计一个系统调用,返回指定进程的相关时间信息的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!