操作系统内核模块编程

实验要求:
①设计一个模块,要求列出系统中所有内核线程的程序名、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_tint型)

为什么用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;
}

测试结果:
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

猜你喜欢

转载自blog.csdn.net/include_not_found_/article/details/80327632