一、知识点摘录
基于linux内核模块(LKM)的方法:
它可以被用于实时跟踪任何进程特定的数据。在这种方法中,用户不需要任何单独的工具,只需要内核模块来收集所有基本信息。此外,这种基于模块的技术的能力可以通过加入额外的功能来增强,从而使其成为一个成熟的多进程抽象层,以满足任何高性能嵌入式平台的需求。
Linux性能分析命令:
Linux实时进程监控实现:
我们的进程监视大致分为两个部分,用户空间数据收集和内核空间模块。字符设备节点(/dev/myNode)用于在这两个单元之间通信。用户空间应用程序打开字符节点并传递适当的请求字符串,以获得进程所需的信息。
二、代码实现
用户空间数据收集系统ucode.c代码如下:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
/*可以检索的进程统计信息列表:*/
void showopt ( ) {
printf ( " 1. process name\n"
" 2. group id\n"
" 3. parent process\n"
" 4. process group leader\n"
" 5. child processes created\n"
" 6. process memory segments\n"
" 7. virtual memory mapping\n"
" 8. process priority\n"
" 9. process state\n"
" 10. cpu used by process\n"
" 11. total fault count of process\n"
" 12. process start time\n"
" 13. process link count \n"
" 14. exit \n");
}
/*如前所述,C源代码的主体请求LKM通过字符设备节点/dev/mynode获取数据。我们将进程id (PID)和请求令牌传递给模块。这个模块检查传递的令牌,并通过传递的相同缓冲区buf将结果传递给用户空间应用程序。在这里,我们没有在写之后使用任何单独的读系统调用,以使其简单和更快。*/
#define MAX 1024
int main () {
char buf[MAX],opt[3],pid[10];
long int rt;
//open device node of LKM
int fd=open("/dev/myNode",O_RDWR);
printf("Enter Process PID: ");
scanf("%s",pid);
showopt();
//get option
while(1){
printf(" Enter Option : " );
scanf("%s",opt);
if(strcmp(opt, "14") == 0) break;
//pass message string to LKM
sprintf( buf, "%s %s",pid,opt); //sprintf的作用是将一个格式化的字符串输出到一个目的字符串中
write(fd,buf,strlen(buf)); //write函数把buf中strlen(buf)个字节写入文件描述符fd所指的文档,成功时返回写的字节数,错误时返回-1.
//print result
if (read(fd, buf, MAX) >= 0){
printf ("%s",buf) ;
}
}
close(fd);
return 0;
}
内核模块代码kcode.c代码如下:
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/semaphore.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/sched/signal.h>
/*在内核代码中,我们首先注册一个动态字符设备驱动程序,并在用户空间中创建设备节点/dev/myNode,用户空间应用程序通过该节点与模块进行通信。*/
static struct class *cl;
static struct cdev *cdev;
dev_t dev;
struct semaphore mysem;
int rpid, opt;
#define MAX 1024
static char mybuff[MAX];
int myopen(struct inode *inode,struct file *file)
{
//阻止同时访问
down_interruptible(&mysem); //这个函数的功能就是获得信号量,如果得不到信号量就睡眠,此时没有信号打断,那么进入睡眠。但是在睡眠过程中可能被信号打断,打断之后返回-EINTR,主要用来进程间的互斥同步。
return 0;
}
int myrelease(struct inode *inode,struct file *file)
{
//释放信号量
up(&mysem); //up()函数的功能是释放信号量sem,释放信号量后,sem的计数器的值将加1。
return 0;
}
ssize_t mywrite(struct file *file,const char *buff,size_t count,loff_t *pos)
{
char *to,*val;
//接收到复制缓冲区
copy_from_user(mybuff,buff,count); //将用户空间的数据拷贝到内核空间,失败返回没有被拷贝的字节数,成功返回0
//get PID and OPTION passed
to= &mybuff[0];
val=strsep(&to," "); //该函数是用来分解字符串为一组字符串。第一个参数为要分解的字符串,第二个字符串为分隔符字符串。
rpid=simple_strtol(val,NULL,10); //将一个字符串转换成unsigend long long型数据。
val=strsep(&to," ");
opt= simple_strtol(val,NULL,10);
return count;
}
/*在内核处理程序mywrite中,我们找到进程的任务结构,它的PID被传递。然后根据从用户空间接收到的选项值查找所需的信息。计算各种进程信息的内核逻辑如下所示。*/
ssize_t myread(struct file *file,char *buff, size_t count, loff_t *pos) {
struct task_struct *task, *t;
struct list_head *trav;
struct vm_area_struct *vma;
char *c;
//find the task_struct
for_each_process(task) {
//compare process pid
if(rpid==task->pid)
break;
}
if(rpid == task->pid) {
switch(opt) {
case 1: // Finding process name
count = snprintf(mybuff, MAX, "%s\n", task->comm);
break;
case 2: // Finding group id
count = snprintf(mybuff, MAX, "%d\n", task->tgid);
break;
case 3: // Finding parent process
count = snprintf(mybuff, MAX, "Process:%s with Pid=%d\n",task->parent->comm, task->parent->pid);
break;
case 4: // Finding group leader
count = snprintf(mybuff, MAX, "Process:%s with Pid=%d\n",
task->group_leader->comm, task->group_leader->pid);
break;
case 5: // Finding child processes created
count = 0;
list_for_each (trav, &(task->children)) {
t = list_entry(trav, struct task_struct, sibling);
count += snprintf(mybuff + count, MAX - count,"%d ", t->pid);
}
count += snprintf(mybuff + count, MAX - count, "\n");
break;
case 6: // Finding process memory segments
count = snprintf(mybuff,MAX,
"Code Segment: 0x%lx - 0x%lx \n"
"Data Segment: 0x%lx - 0x%lx \n"
"Heap Segment: 0x%lx \n"
"Stack Segment: 0x%lx \n",
task->mm->start_code,
task->mm->end_code,
task->mm->start_data,
task->mm->end_data,
task->mm->start_brk,
task->mm->start_stack
);
break;
case 7: // Finding process Virtual memory mapping
count = snprintf(mybuff, MAX, "Total no of vmas=%d\n",task->mm->map_count);
for(vma = task->mm->mmap; vma; vma = vma->vm_next) {
count += snprintf(mybuff + count,MAX - count, "0x%lx - 0x%lx\n",vma->vm_start, vma->vm_end);
}
break;
case 8: // Finding process priority
count = snprintf(mybuff, MAX,
"Static Priority(nice)=%d\n"
"Dynamic Priority=%d\n"
"Normal Priority=%d\n",
task->prio, task->static_prio, task->normal_prio
);
break;
case 9: // Finding process scheduling policy
switch(task->policy) {
case SCHED_NORMAL:
c = "SCHED_NORMAL\n";
break;
case SCHED_FIFO:
c = "SCHED_FIFO\n";
break;
case SCHED_RR:
c = "SCHED_RR\n";
break;
case SCHED_BATCH:
c = "SCHED_BATCH\n";
break;
case SCHED_IDLE:
c = "SCHED_IDLE\n";
break;
case SCHED_DEADLINE:
c = "SCHED_DEADLINE\n";
break;
}
count = snprintf(mybuff, MAX, "%s", c);
break;
case 10: // Finding cpu used
count = snprintf(mybuff, MAX, "CPU used=%d\n",task_cpu(task));
break;
case 11: // Finding major and minor faults
count = snprintf(mybuff, MAX,
"No of Major faults=%ld\n"
"No of Minor faults=%ld\n",
task->maj_flt, task->min_flt
);
break;
case 12: // Finding process start time
/*经查询man手册,start_time的单位为时钟滴答即毫秒,经以下代码计算算出来的时间和开始时间正好吻合*/
count = snprintf(mybuff, MAX,
"TIME: %.2lld:%.2lld:%.2lld \r\n",
task->start_time/1000/3600%24,
task->start_time/1000/60%60,
task->start_time/1000%60
);
break;
case 13: // Finding process link count
count = snprintf(mybuff, MAX, "Link count=%d\n",
refcount_read(&(task->thread_pid->count)));
break;
default:
count = 20;
strncpy(mybuff, "option not exist\n", count);
printk("%s\n", mybuff);
break;
}
} else {
count = 20;
strncpy(mybuff, "PID not exist\n", count);
printk("%s\n", mybuff);
}
copy_to_user(buff, mybuff, count+1);//将内核空间中的数据拷贝到用户空间
return count;
}
/*所有对驱动模块的访问,如打开、写入、关闭/dev/myNode,都是通过内核接口结构文件操作进行的。信号量mysem用于单例访问模块。*/
struct file_operations fops ={
.owner=THIS_MODULE,
.open=myopen,
.release=myrelease,
.write= mywrite,
.read = myread,
};
static int start(void)
{
printk(KERN_ALERT "Initializing Module"); //KERN_ALERT为消息打印级别
//get major no
alloc_chrdev_region(&dev,0,1,"myNode") ; //该函数是用来向内核申请主设备号时用的,第一个参数:输出型参数,获得一个分配到的设备号
// regisiter device
cdev =cdev_alloc(); //申请一个字符设备
cdev->ops=&fops; //文件操作
cdev->owner=THIS_MODULE;
cdev_add(cdev,dev,1); //添加一个字符设备到操作系统
// 创建设备节点
cl=class_create(THIS_MODULE,"myNode"); //此函数的执行效果就是创建一个新的文件夹,此文件夹的名字为此函数的第二个输入参数,但此文件夹是空的
device_create(cl,NULL,cdev->dev,NULL,"myNode"); //该函数进行设备文件创建
//initialize semaphore
sema_init(&mysem,1); //初始化信号量,将信号量计数器值设置1。
return 0;
}
/*这里使用信号量(mysem)实现从同步访问到模块的同步。根据设计,该模块一次服务单个请求,而其他请求必须等待才能获得访问。*/
static void leave(void)
{
printk(KERN_ALERT "Leaving Module\n") ;
device_destroy(cl,dev);
class_destroy(cl);
cdev_del(cdev);
unregister_chrdev_region(cdev->dev,1);
}
module_init(start);
module_exit(leave);
MODULE_LICENSE("GPL");
Makefile代码如下:
obj-m += kcode.o
CURRENT_PATH:=$(shell pwd) #模块所在的当前所在路径
LINUX_KERNEL:=$(shell uname -r) #linux内核代码的当前版本
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL) #linux内核的当前版本源码路径
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules #编译模块
# 内核的路径 当前目录编译完放哪 表明编译的是内核模块
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean #清理模块
运行结果:
程序的理解:
该程序中涉及到了很多有关设备操作的函数,仅能对该函数的功能进行理解,具体实现有点不太明白,对于信号量方面也很是陌生,但极大的锻炼了我解决报错和修改代码的能力。
在我看来,这个程序的具体实现过程为:在内核中创建了一个字符设备,该设备可以写入用户端程序传入到/dev/myNode中的数据,然后读出相应的数据到/dev/myNode,最后由用户端程序进行输出