实验要求:
①设计一个模块,要求列出系统中所有内核线程的程序名、PID、进程状态、进程优先级、父进程PID
②设计一个带参数的模块,参数为某个进程的PID号,模块功能是列出该进程的家族信息,包括父进程、兄弟进程和子进程的程序名、PID号码、进程状态
开始实验前先了解一下内核模块编程的基本概念和基本框架。
①内核模块编程是什么,有什么好处?
Linux是整体式结构,相对于微内核结构(模块化的结构)而言,运行效率高,但系统可维护性差。Linux为此提供了内核模块机制,可以弥补一些不足而不影响系统性能。其本身不是一个单独的进程,不能独立运行,但可以动态载入内核,成为内核代码的一部分。在不需要其功能时,可以动态卸载。修改模块代码后只需要重新编译和加载模块,不用重新编译内核,引导系统,降低系统更新难度。
②模块的组成:模块代码+编译文件
模块代码构成:头文件声明、模块许可证声明、初始化和清理函数
初始化和清理函数声明为static类型(只能在当前源码文件下可用),不会在特定文件之外可用。且清理函数必须被声明为void类型,因为它返回值没有意义。模块需要传入参数的情况下需要加上头文件#include <linux/moduleparam.h>
,且需要声明访问权限。带模块参数在insomd的时候需要在.ko文件后紧跟上参数值。
#include<linux/init_h>
#include<linux/module.h>
#include<linux/kernel.h>
#include <linux/moduleparam.h>//模块有参数
static type param;//模块参数
module_param(name,type,perm);//传入参数姓名,类型和其访问权限
static type xxx_init(param){};
static type xxx_exit(param){};
module_init( xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE(“GPL”);//模块许可证
编译文件Makefile构成:
obj-m:=xxxx.o//生成的模块为xxxx.ko
xxxx-objs:=test.o//<模块名>-objs:=<目标文件>
//若模块由多个c文件构成,模块名不能和目标文件名相同
KDIR:=/lib/modules/$(shell uname -r)/build //KDIR指向当前内核源码目录
PWD:=$(shell pwd)//PWD是目前所在工作目录的绝对路径
default:
make -C $(KDIR) M = $(PWD)modules; //-C指定内核源码目录,M指定源码目录
clean:
make -C $(DIR) M=$(PWD)clean;
那么遍历进程、列出进程家族成员信息功能如何实现呢?
思路:
利用进程描述符task_struct中的state、prio、comm(进程名)、parent、children、sibling等成员,以及for_each_process,list_for_each,list_entry等宏去访问链表。
父进程只有一个,子进程、兄弟进程有多个。
comm:去除路径后的可执行文件名称
state定义:
volatile long state;
int exit_state;
#define TASK_RUNNING 0 //运行态
#define TASK_INTERRUPTIBLE 1 //可唤醒的阻塞态
#define TASK_UNINTERRUPTIBLE 2 //不可唤醒的阻塞态
#define __TASK_STOPPED 4 //进程被停止执行
#define __TASK_TRACED 8 //进程功能处于被追踪状态
……
linux链表:(双向循环链表)
struct list_head{
struct list_head *next,*prev;
}
for_each_process宏:
#define for_each_process(p)\
for(p=&init_task;(p=next_task(p))!=&init_task;)
其中init_task指向内核第1个进程号
list_for_each宏://找到链表结点中list_head域的位置pos
#list_for_each(pos,head)\
for(pos=(head)->next;pos!=(head);pos=pos->next)
list_entry宏://获取指针所在结构体变量的首地址
#define list_entry(ptr,type,member)\
container_of(ptr,type,member)
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
#define offsetof(type, member) ((size_t) &((TYPE*)0)->member)
1>( (TYPE *)0 ) 0地址强制 "转换" 为 TYPE结构类型的指针;
2>((TYPE *)0)->MEMBER 访问TYPE结构中的MEMBER数据成员;
3>&( ( (TYPE *)0 )->MEMBER)取出TYPE结构中的数据成员MEMBER的地址;
4>(size_t)(&(((TYPE*)0)->MEMBER))结果转换为size_t类型(siez_t是int型)
为什么用0强转:虚拟地址映射,程序内存空间首地址以0开始
container_of首先计算出type结构体中成员member在结构体中偏移量,然后用ptr减去这个偏移量
写代码前参考文章:
https://blog.csdn.net/u013904227/article/details/50931540
https://blog.csdn.net/jiatingqiang/article/details/6437496
https://blog.csdn.net/murphykwu/article/details/47417047
https://blog.csdn.net/naedzq/article/details/51723538
模块1:
通过for_each_process宏遍历所有进程,然后根据进程的mm值判断是否为内核进程,并设置count值对内核进程计数
#include<linux/sched.h>//task_struct等的头文件
#include<linux/init.h>
#include<linux/kernel.h>
#include<linux/module.h>
#include <linux/init_task.h>
static int traverse_init(void)
{
int count=0;
struct task_struct *p;//进程结构体指针
printk(KERN_ALERT"名称 PID 状态 优先级 父进程PID\n");
for_each_process(p)
{
if(p->mm==NULL){
printk(KERN_ALERT"%-25s%-8d%-8ld%-8d%-8d\n",p->parent->comm,p->parent-> pid,p->parent->state,p->prio,p->parent->pid);
count++;
}
}
printk("The number of the total processes:%d\n",count);
return 0;
}
static void traverse_exit(void)
{
printk(KERN_ALERT"goodbye!\n");
}
module_init(traverse_init);
module_exit(traverse_exit);
MODULE_LICENSE("GPL");
源码当前目录下打开终端,执行以下命令
make
sudo insomd test.ko
dmesg
输出结果:
卸载模块观察输出
sudo rmmod test.ko
dmesg
终端输出结果
模块2:
传入的参数为int类型的pid号,通过find_get_pid得到struct pid*类型指针,再通过pid_task函数得到pid号对应的进程。task_struct进程描述符中parent为struct task_struct 类型(父进程只有一个)。而sibling和childre为struct list_head 类型,所以要用到list_for_each和list_entry宏进行遍历。
#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/sched.h>
#include<linux/moduleparam.h>
static int pid;
module_param(pid,int,0644);
static int lTF_init(void)//列出某个进程父/兄弟/子进程的名字,PID,状态
{
struct task_struct *p,*s;
struct list_head *pos;//链表位置指针
struct pid *id;
id=find_get_pid(pid);//得到对应的struct pid
if(id==NULL)
{
printk("该PID对应的进程不存在!\n")
return 0;
}
else
{
p=pid_task(id,PIDTYPE_PID);//通过stuct pid得到进程的结构体
printk("该进程PID号:%d\n",pid);
if(p->parent!=NULL)//父进程信息,parent是指针
{
printk("父亲进程名称 PID 状态\n");
printk("%-25s%-8d%-8ld\n",p->comm,p->pid,p->state);
}
else printk("该进程没有父进程!\n");
/*兄弟进程信息*/
if((p->sibling).next!=&p->sibling)
{
printk("兄弟进程名称 PID 状态\n");
list_for_each(pos,&p->sibling)
{
s=list_entry(pos,struct task_struct,sibling);
printk("%-25s%-8d%-8ld\n",s->comm,s->pid,s->state);
}
}
else printk("该进程没有兄弟进程!\n");
/*孩子进程信息*/
if((p->children).next!=&p->children)
{
printk("孩子进程名称 PID 状态\n");
list_for_each(pos,&p->children)
{
s=list_entry(pos,struct task_struct,sibling);
printk("%-25s%-8d%-8ld\n",s->comm,s->pid,s->state);
}
}
else printk("该进程没有孩子进程!\n");
return 0;
}
}
static void lTF_exit(void)
{
printk(KERN_ALERT"Thanks for use,goodbye!\n");
}
module_init(lTF_init);
module_exit(lTF_exit);
MODULE_LICENSE("GPL");
执行命令
make
sudo insmod xxx.ko pid=12
执行结果:
卸载时输出:
实验问题总结:
①
为什么有的输出背景是红色?
经过观察后发现,printk中传入参数KERN_ALERT便会使该句输出背景为红色,printk中的参数代表日志级别,printk据此将指定信息输出到控制台或者日志文件中
②
第二个模块实验结果中还有一个小问题而且无法解决,比如会产生下列错误输出。
猜测:遍历时头结点没有被跳过,输出了错误地址,输出的信息是一串地址。下一个节点是pid=0 的进程。网上没有搜索到相关问题,自己尝试找到该结点的前一个结点来调试观察输出,发现输出结果与预想结果并不符。暂时不知如何解决。
最后,实验外,自己瞎折腾系列:尝试用C语言+shell编写一个用户级程序
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(){
char path1[]={"sh ./listAllThread/SHOW.sh"};
char command1[]={"sudo rmmod ./listOneProInfo/lPF.ko"};
char s,pid[5];
printf("请选择操作:\n");
do{
printf("<1>列出所有内核线程信息 <2>读取某个进程家族信息\n");
s=getchar();
switch(s){
case '1':system(path1);break;
case '2':
{
char command2[]={"sudo insmod ./listOneProInfo/lPF.ko pid="};
system(command1);
printf("请输入你想查看的进程的pid号:\n");
scanf("%s",pid);
strcat(command2,pid);
system(command2);
system("dmesg |tail -80");
break;
}
default:{printf("输入错误,请重试!\n");continue;}
}
getchar();
}while(1);
return 0;
}
测试结果: