操作系统课程实践设计 实验一 代码详解
获取与修改优先级和nice
实验一的内容是nice值的修改与获取,我将flag提取出来做出了两个getnice和modinice的功能
getnicesyscall
声明和添加调用号的过程不做重述,以下仅将实现部分的代码进行讲解。
1 | SYSCALL_DEFINE4(getnicesyscall, pid_t , pid, int, nicevalue,void __user * ,prio,void __user * ,nice) |
pid_t
:pid_t的源码在/usr/include/sys/types.h中
1 | #include <bits/types.h> |
第一个替换:pid_t -> __pid_t
:__pid_t的源码/usr/include/bits/types.h中
1 | #include <bits/typesizes.h> |
第二个替换:__pid_t -> extension typedef __PID_T_TYPE
:__PID_T_TYPE 在/usr/include/bits/typesizes.h
1 |
|
第三个替换 __PID_T_TYPE ->__S32_TYPE
:__S32_TYPE
/usr/include/bits/types.h
1 | #define __S32_TYPE int |
pid_ ----> int
我他妈…
绕这么大一圈 为啥不直接就是int呢??
struct pid
struct pid 是进程描述符,在include/linux/pid.h 中:
1 |
|
task_struct
task strcut 无论是在哪个实验中都是十分重要的一个存在,在这里详细讲一下task_struct结构体。
其定义在include/linux/sched.h中。
需要注意的是:
task_struct包含很多内容:
1标示符: 描述本进程的唯一标识符,用来区别其他进程。
状态 :任务状态,退出代码,退出信号等。
优先级 :相对于其他进程的优先级。
程序计数器:程序中即将被执行的下一条指令的地址。
内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
上下文数据:进程执行时处理器的寄存器中的数据。
I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
另外,task_strcut是以链表的形式存在内核中的。
本文仅略微描述功能和代码,具体task_strcut
我会另外新一篇博客中详细写。
1.状态
volatile long state
进程状态中可以看到在操作系统理论课中有讲到的zombie进程,DEAD进程等等。
1 |
|
仅展示一部分 。
2.进程标识(pid)
pid_t pid
pid_t tpid
pid是进程的唯一标识,tpid是领头线程的pid成员的值。
领头线程 是指:
一个线程组中的所有线程使用和该线程组的领头线程(该组中的第一个轻量级进程)相同的PID,并被存放在tgid成员中。只有线程组的领头线程的pid成员才会被设置为与tgid相同的值。
getpid()系统调用返回的是当前进程的tgid值而不是pid值。(线程是程序运行的最小单位,进程是程序运行的基本单位。)此部分在后文详述。
3.进程标记
unsigned int flags
此部分不进行详述,实验中涉及很少,在以后的task_struct详细解析。
4.进程间的亲属关系
1 | struct task_struct *real_parent; /* real parent process */ |
在Linux系统中,所有进程之间都有着直接或间接地联系,每个进程都有其父进程,也可能有零个或多个子进程。拥有同一父进程的所有进程具有兄弟关系。
real_parent指向其父进程,如果创建它的父进程不再存在,则指向PID为1的init进程。
parent指向其父进程,当它终止时,必须向它的父进程发送信号。它的值通常与** real_parent**相同。
children表示链表的头部,链表中的所有元素都是它的子进程(进程的子进程链表)。
sibling用于把当前进程插入到兄弟链表中(进程的兄弟链表)。
group_leader指向其所在进程组的领头进程。
成员 | 描述 |
---|---|
real_parent | 指向当前操作系统执行进程的父进程,如果父进程不存在,指向pid为1的init进程 |
parent | 指向当前进程的父进程,当当前进程终止时,需要向它发送wait4()的信号 |
children | 表示链表的头部,链表中的所有元素都是它的子进程(进程的子进程链表)。 |
sibling | 用于把当前进程插入到兄弟链表中(进程的兄弟链表) |
group_leader | 指向其所在进程组的领头进程。 |
在后续的遍历进程的实验中,task_struct中此部分应用比较广泛。
5.进程的调度信息
1 | int prio, static_prio, normal_prio; |
实时优先级范围是0到MAX_RT_PRIO-1(即99)
,而普通进程的静态优先级范围是从MAX_RT_PRIO到MAX_PRIO-1(即100到139)
。值越大静态优先级越低
。
static_prio
保存的是静态优先级。
rt_priority
保存实时优先级
normal_prio
值的大小取决于静态优先级
和调度策略
(进程的调度策略有:先来先服务,短作业优先、时间片轮转、高响应比优先等等的调度算法。
prio
保存动态优先级。我们通过nice值修改的也是这个优先级。算法是prio+=nice
。所以nice值修改进程优先级的时候,一般赋给负值来提高优先级。
policy
保存进程的调度策略。
调度策略:
1 |
6.ptrace系统调用
跟踪进程时应用,此处不做详述。
7.时间数据成员
1 | cputime_t utime, stime, utimescaled, stimescaled; |
8.进程地址空间
1 | struct mm_struct *mm, *active_mm; |
在打印内核线程时候,用到的就是mm==NULL的条件判断的。
成员 | 描述 |
---|---|
mm | 进程所拥有的内存空间描述符,对于内核线程的mm为NULL |
active_mm | 指进程运行时所使用的进程描述符 |
rss_stat | 被用来记录缓冲信息 |
注意:如果当前内核线程被调度之前运行的也是另外一个内核线程时候,那么其mm和avtive_mm都是NULL。
find_get_pid
有了中间很长一部分的task_struct的解析,一下部分应该会容易理解很多。
find_get_pid
1 | struct pid *find_get_pid(pid_t nr) |
find_get_pid(pid_t nr) 是通过进程号pid_t nr得到进程描述符,并将结构体中的count加1.
在这里,find_vpid
返回的是进程描述符。get_pid
是让count+1。
总之,kpid返回的是一个pid类型的结构体,其定义在task_struct中。
pid_task
pid_task(pid* pid,PIDTYPE_PID)
可以通过进程描述符找到task_struct* task,简而言之,就是返回当前进程标识符的task_struct结构体。
task_nice
1 | static inline int task_nice(const struct task_struct *p) |
通过当前task的静态优先级的值减去DEFAULT_PRIO。DEFAULT_PRIO在前面的博文中已经分析过,其值是120.
task_prio
同理。
copy_to_user
由于内核空间与用户空间的内存不能直接互访,所以用此函数。
1 | // ./include/linux/uaccess.h |
modinicesyscall
前半部分和get基本相同,就是后边加上了set的函数。
1 | //Modify nice |
set_user_nice
set_user_nice源码解释起来比较复杂,简言之,根据 task_struct 确定一个进程,并改变进程 nice 值。
1 | void set_user_nice(struct task_struct *p, long nice) |
myapi
说来惭愧…
当初想打印计算机信息结果能力超出范围,于是就变成了一个math库里的函数
return m^2
太惭愧了。
Query
nicevalue的作用:
在设计这个get和modify的时候,本意是想着两个放到一起,用flag的形式装上去,然后听完课之后突然发现功能模块化也是很不错的,然后就在原来flag的基础上改了一下,两个系统调用用的都是传进来的nicevalue
。
实际上在getnicesyscall中的nicevalue没有任何作用
这是我在写代码时犯下的错误,不建议模块化的同学们这样写。
此外,在传递参数时候一定考虑到,分模块的功能就是为了在每个子函数能够使用尽量少的参数,所以一定要精简。
第二个花括号:
现在已经注释掉了,原来在从报告向md文档粘贴时候少了后半段代码,我就直接给后面加了一个花括号,后来在检查的时候发现get的后半段没粘贴上就手动又重新粘贴了后半段,中间的}
就没删…
太惭愧了。
传递的pid不存在:
对于程序健全性来说,pid
不为空的检查是必要的,但是很多代码都没这么写,包括我在内。
后来在各类资料中查找才知道,如果用户给出的pid没有与之对应的进程,find_get_pid会返回一个NULL指针。
关于copy_to_user的再理解
内核和用户空间应用程序具有不同的地址空间,因此复制到用户空间需要更改地址空间。每个进程都有自己的(用户)地址空间。
另外,复制到用户空间时,内核不应该崩溃,所以copy_to_user
函数可能会检查目标地址是否有效(也许该地址应该从分区空间中分页)。
getpid和putpid
getpid的作用就是获得当前进程的进程标识符(pid)
putpid的源码:
1 | void put_pid(struct pid *pid) |
在每次get_pid时候,最后最好都要put_pid一下,因为由get_pid的含义可以知道,应用atomic_inc(&pid->count);
每次都会给task中count字段自增1,而put_pid则是atomic_dec_and_test(&pid->count)
用来减1来达到释放资源的目的(引用计数,当被引用一次就自增1,每次调用pud_pid用来给count字段减1,最后字段为0时候释放资源)