Linux进程管理:深入task_struct字段

科技布道师 2024-03-07 01:27:17

一、通过top命令可以看到进程的相关信息

在 Ubuntu 下,top 命令可以监视即时的进程状态。通过man top查看了top的基本用法,在 top 中,按 u,再输入你的用户名,可以限定只显示以你的身份运行的进程,更方便观察。按 h 可得到帮助。

二、打印task_struct字段信息

2.1、探索task_struct字段:

操作系统为了对进程更好的管理,专门用一个结构体来保存进程的相关的信息,这个结构体叫task_struct,在源码中查找有关这个结构体的信息,在/include/linux/sched.h中定义如下:

struct task_struct {unsigned int __state;/* * This begins the randomizable portion of task_struct. Only * scheduling-critical items should be added above here. */ randomized_struct_fields_startvoid *stack;refcount_t usage;/* Per task flags (PF_*), defined further below: */unsigned int flags;unsigned int ptraceint on_rq;int prio;int static_prio;int normal_prio;unsigned int rt_priority;struct sched_entity se;struct sched_rt_entity rt;struct sched_dl_entity dl;const struct sched_class *sched_class;struct sched_statistics stats;unsigned int policy;int nr_cpus_allowed;const cpumask_t *cpus_ptr;cpumask_t *user_cpus_ptr;cpumask_t cpus_mask;void *migration_pending;unsigned short migration_flags;struct sched_info sched_info;struct list_head tasks; //指向进程PCB的指针struct mm_struct *mm;struct mm_struct *active_mm;int exit_state;int exit_code;int exit_signal;/* The signal sent when the parent dies: */int pdeath_signal;/* JOBCTL_*, siglock protected: */unsigned long jobctl;/* Used for emulating ABI behavior of previous Linux versions: */unsigned int personality;/* Scheduler bits, serialized by scheduler locks: */unsigned sched_reset_on_fork:1;unsigned sched_contributes_to_load:1;unsigned sched_migrated:1;/* Force alignment to the next boundary: */unsigned :0;/* Unserialized, strictly 'current' *//* * This field must not be in the scheduler word above due to wakelist * queueing no longer being serialized by p->on_cpu. However: * * p->XXX = X; ttwu() * schedule() if (p->on_rq && ..) // false * smp_mb__after_spinlock(); if (smp_load_acquire(&p->on_cpu) && //true * deactivate_task() ttwu_queue_wakelist()) * p->on_rq = 0; p->sched_remote_wakeup = Y; * * guarantees all stores of 'current' are visible before * ->sched_remote_wakeup gets used, so it can be in this word. */unsigned sched_remote_wakeup:1;/* Bit to tell LSMs we're in execve(): */unsigned in_execve:1;unsigned in_iowait:1;unsigned long atomic_flags; /* Flags requiring atomic access. */struct restart_block restart_block;pid_t pid; //进程pidpid_t tgid //进程的线程pid/* Real parent process: */struct task_struct __rcu *real_parent; //亲生父亲进程/* Recipient of SIGCHLD, wait4() reports: */struct task_struct __rcu *parent; //养父进程/* * Children/sibling form the list of natural children: */struct list_head children; //子进程链表struct list_head sibling; //兄弟进程链表struct task_struct *group_leader; //线程组的头进程/* * 'ptraced' is the list of tasks this task is using ptrace() on. * * This includes both natural children and PTRACE_ATTACH targets. * 'ptrace_entry' is this task's link on the p->parent->ptraced list. */struct list_head ptraced;struct list_head ptrace_entry;/* PID/PID hash table linkage. */struct pid *thread_pid;struct hlist_node pid_links[PIDTYPE_MAX];struct list_head thread_group;struct list_head thread_node;struct completion *vfork_done;/* CLONE_CHILD_SETTID: */int __user *set_child_tid;/* CLONE_CHILD_CLEARTID: */int __user *clear_child_tid;/* PF_KTHREAD | PF_IO_WORKER */void *worker_private; u64 utime; u64 stime; u64 gtime;struct prev_cputime prev_cputime;/* Context switch counts: */unsigned long nvcsw;unsigned long nivcsw;/* Monotonic time in nsecs: */ u64 start_time;/* Boot based time in nsecs: */ u64 start_boottime;/* MM fault and swap info: this can arguably be seen as either mm-specific or thread-specific: */unsigned long min_flt;unsigned long maj_flt;/* Empty if CONFIG_POSIX_CPUTIMERS=n */struct posix_cputimers posix_cputimers;/* Process credentials: *//* Tracer's credentials at attach: */const struct cred __rcu *ptracer_cred;/* Objective and real subjective task credentials (COW): */const struct cred __rcu *real_cred;/* Effective (overridable) subjective task credentials (COW): */const struct cred __rcu *cred;char comm[TASK_COMM_LEN]; // 可执行程序的名字,包含路径struct nameidata *nameidata /* Filesystem information: */struct fs_struct *fs;/* Open file information: */struct files_struct *files /* Namespaces: */struct nsproxy *nsproxy;/* Signal handlers: */struct signal_struct *signal;struct sighand_struct __rcu *sighand;sigset_t blocked;sigset_t real_blocked;/* Restored if set_restore_sigmask() was used: */sigset_t saved_sigmask;struct sigpending pending;unsigned long sas_ss_sp;size_t sas_ss_size;unsigned int sas_ss_flags;struct callback_head *task_worksstruct seccomp seccomp;struct syscall_user_dispatch syscall_dispatch;/* Thread group tracking: */ u64 parent_exec_id; u64 self_exec_id;/* Protection against (de-)allocation: mm, files, fs, tty, keyrings, mems_allowed, mempolicy: */spinlock_t alloc_lock;/* Protection of the PI data structures: */raw_spinlock_t pi_lock;struct wake_q_node wake_q /* Journalling filesystem info: */void *journal_info;/* Stacked block device info: */struct bio_list *bio_list;/* Stack plugging: */struct blk_plug *plug;/* VM state: */struct reclaim_state *reclaim_state;struct io_context *io_context;/* Ptrace state: */unsigned long ptrace_message;kernel_siginfo_t *last_siginfo;struct task_io_accounting ioac;struct tlbflush_unmap_batch tlb_ubc;/* Cache last used pipe for splice(): */struct pipe_inode_info *splice_pipe;struct page_frag task_frag;/* * When (nr_dirtied >= nr_dirtied_pause), it's time to call * balance_dirty_pages() for a dirty throttling pause: */int nr_dirtied;int nr_dirtied_pause;/* Start of a write-and-pause period: */unsigned long dirty_paused_when;/* * Time slack values; these are used to round up poll() and * select() etc timeout values. These are in nanoseconds. */ u64 timer_slack_ns; u64 default_timer_slack_ns;struct rcu_head rcu;refcount_t rcu_users;int pagefault_disabled;};2.2、打印task_struct字段2.2.1、代码设计思路:

系统中的进程数量巨大,为了方便管理,于是推出了进程链表的概念,每个进程链表由指向PCB的指针组成,在struct task_struct中定义为tasks字段。其大概结构如图所示:

其中进程链表的头指针和尾指针均是init_task,这个PCB是0号进程的,0号进程是一直存在于系统中的,不会被撤销。因此可以通过以前学习的链表的相关知识,遍历系统中的进程链表,进而访问每一个进程的PCB,从而打印进程的相关信息。

可以看出来,task_struct的成员有很多个,在这块重点了解以下几个属性:

cur->pid 进程号cur->comm 进程名cur->__state 进程状态cur->exit_state 进程退出的状态cur->exit_code 进程正常终止的状态码cur->exit_signal 进程异常终止的信号(cur->parent)->pid 父进程的pid(cur->parent)->comm 父进程名(cur->real_parent)->pid 亲生父亲进程的pid(cur->real_parent)->comm 亲生父亲进程名cur->children; 子进程链表cur->sibling; 兄弟进程链表//utime和stime单位均为jiffies,它在 kernel/sched.c 文件中定义为一个全局变量:long volatile jiffies=0;它记录了从开机到当前时间的时钟中断发生次数typedef unsigned long long u64;u64 utime;//运行在用户空间的CPU时间u64 stime;//运行在内核空间的CPU时间2.2.2、编写代码(无传参):

Makefile

obj-m +=prmod.oCURRENT_PATH:=$(shell pwd)LINUX_KERNEL:=$(shell uname -r)LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)all: make -C/lib/modules/$(shell uname -r)/build M=$(PWD) modulesclean: make -C/lib/modules/$(shell uname -r)/build M=$(PWD) clean

prmod.c

#include <linux/module.h>#include <linux/init.h>#include <linux/init_task.h>#include<linux/list.h>#include <linux/sched.h>#include <linux/types.h>#include <linux/kernel.h> //入口函数static int __init my_print_init(void){ struct task_struct *task,*p; struct list_head *pos; int count = 0;//计数器count printk("there are some infomation about processes\n"); task = &init_task;//task设为双指针的头节点,让它指向0进程的PCB list_for_each(pos,&task->tasks){//从双链表的头开始遍历, p=list_entry(pos,struct task_struct,tasks);//找到结构体struct task_struct的tasks字段所在的结构体地址,即找到该进程的PCB count++; printk("第%d个进程信息如下:\n",count); printk("name: %s, pid: %d, state: %d, exit_state: %d, exit_code: %d, exit_signal: %d, parent_pid: %d, parent_name: %s, utime: %d, stime: %d\n",p->comm,p->pid,p->__state,p->exit_state,p->exit_code,p->exit_signal,(p->parent)->pid,(p->parent)->comm,p->utime,p->stime); } printk("总共有%d个进程\n",count); return 0;}//出口函数static void __exit my_print_exit(void){ printk("Finished!\n");}module_init(my_print_init);module_exit(my_print_exit);MODULE_LICENSE("GPL");

运行结果

三、传参访问特定的进程3.1、find_get_pid()、pid_task()源码分析:

要实现对进程的快速查找,链表相对来说是要花费大量时间的,因此==引入了哈希表==的概念。这是==通过哈希函数把进程的pid转化成表的索引,这部分linux使用了宏pid_hashfn来实现==。而==linux当中提供了一些从pid获取到pcb的接口函数==,例如find_get_pid()和pid_task()。

在源码当中查找,发现find_get_pid定义在/kernel/pid.c中

struct pid *find_get_pid(pid_t nr){ struct pid *pid; //定义了一个pid的结构体 //RCU下可访问 rcu_read_lock(); pid = get_pid(find_vpid(nr)); rcu_read_unlock(); return pid;}

再查看find_vpid和get_pid的代码:

//通过进程号和进程命名空间指针来找到对应的pid结构体指针struct pid *find_vpid(int nr){ return find_pid_ns(nr, task_active_pid_ns(current));}//idr是映射器,给定nr和命名空间指针来在idr中查找对应的pid结构体指针struct pid *find_pid_ns(int nr, struct pid_namespace *ns){ return idr_find(&ns->idr, nr); /** * idr_find() - Return pointer for given ID. * @idr: IDR handle. * @id: Pointer ID. * * Looks up the pointer associated with this ID. A % pointer may * indicate that @id is not allocated or that the % pointer was * associated with this ID. * * This function can be called under rcu_read_lock(), given that the leaf * pointers lifetimes are correctly managed. * * Return: The pointer associated with this ID. */}//返回给定task对应的进程命名空间指针struct pid_namespace *task_active_pid_ns(struct task_struct *tsk){ return ns_of_pid(task_pid(tsk));}//返回当前的pid结构体指针的进程命名空间,如果当前的pid指针不存在,则给它赋值static inline struct pid_namespace *ns_of_pid(struct pid *pid){ struct pid_namespace *ns = ; if (pid) ns = pid->numbers[pid->level].ns; return ns;}//返回task结构体对应的的线程pid指针static inline struct pid *task_pid(struct task_struct *task){ return task->thread_pid;

find_vpid的调用顺序如下:

get_pid代码:

static inline struct pid *get_pid(struct pid *pid){ if (pid) refcount_inc(&pid->count);//增加结构体的引用计数 return pid; //返回该pid结构体}

因此,find_get_pid是给定pid号找到pid号对应的struct pid指针

pid_task都定义在kernel/pid.c中,查看源码:

struct task_struct *pid_task(struct pid *pid, enum pid_type type){ struct task_struct *result = ; if (pid) { struct hlist_node *first; //rcu_dereference_check() 是一个用于读取RCU保护数据的宏 first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]), //lockdep_tasklist_lock_is_held() 是一个用于判断当前任务列表锁是否被持有的函数。 lockdep_tasklist_lock_is_held()); if (first) //通过给定的pid类型及first,找到对应的链表节点所在的结构体的指针,并保存在result返回 result = hlist_entry(first, struct task_struct, pid_links[(type)]); } return result;}//哈希表头节点struct hlist_head { struct hlist_node *first;};struct hlist_node { struct hlist_node *next, **pprev;};//hlist_first_rcu(head)是取到哈希表的头指针的头节点#define hlist_first_rcu(head) (*((struct hlist_node __rcu **)(&(head)->first)))//通过member来获取它对应的type的指针#define hlist_entry(ptr, type, member) container_of(ptr,type,member)

==综上所述,通过find_get_pid获取指定pid的pid结构体指针,再通过pid_task查找哈希表,并返回对应的PCB,然后就可以访问该进程的一切信息啦==

3.2、编写代码(有传参):

code8.c

#include <linux/kernel.h>#include <linux/init.h>#include <linux/module.h>#include <linux/sched.h> #include <linux/types.h>#include<linux/pid.h>int my_pid = 5;module_param(my_pid,int,0644);static int __init my_test_init(void){ struct pid* pid = find_get_pid(my_pid); struct task_struct *p; p = pid_task(pid,PIDTYPE_PID); printk("pid为%d的信息如下:\n",my_pid);if(p){ printk("name: %s, pid: %d, state: %d, exit_state: %d, exit_code: %d, exit_signal: %d, parent_pid: %d, parent_name: %s, utime: %d, stime: %d\n",p->comm,p->pid,p->__state,p->exit_state,p->exit_code,p->exit_signal,(p->parent)->pid,(p->parent)->comm,p->utime,p->stime);} return 0;}static void __exit my_test_exit(void){ printk("goodbye\n");}module_init(my_test_init);module_exit(my_test_exit);MODULE_LICENSE("GPL");

Makefile

obj-m +=code8.oCURRENT_PATH:=$(shell pwd)LINUX_KERNEL:=$(shell uname -r)LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)all: make -C/lib/modules/$(shell uname -r)/build M=$(PWD) modulesclean: make -C/lib/modules/$(shell uname -r)/build M=$(PWD) clean

运行结果

插入模块后直接打印信息,打印的为pid为5的进程信息。

在插入模块的时候,重设要打印的pid为2,则打印pid为2的进程信息。

四、打印子进程及兄弟进程:4.1、parent/children/sibling 三者的关系:

在打印特定进程的子进程信息中,发现需要用到sibling链表,所以对 parent/children/sibling 三者的关系产生了疑惑。在《深入理解Linux内核》中有如下一图表明 task_struct 中 parent/children/sibling 三者的关系

上图清晰的表明了task_struct结构中的parent、children、sibling之间的关系,可以看到:

(1)sibling.next是当兄弟进程存在时,就指向下一个兄弟进程的sibling成员,若兄弟进程不存在,则指向parent。而sibling.prev是指向前一个兄弟进程的sibling成员,但若没有上一个进程,则指向parent。

(2)children.next是指向parent的第一个子进程的sibling成员,而children.prev是指向parent的最后一个子进程的sibling成员。

4.2、实现思路:

打印兄弟进程这块有两种思路:

第一种是通过指向当前进程的父亲进程,由上面这块的children和parent之间的特殊关系----父进程的children是指向父进程的第一个子进程的sibling成员,因此可以通过这种方式打印当前进程的所有兄弟进程信息了

第二种思路是通过当前进程的sibling成员直接进行打印,但是在前期打印过程当中,编译成功但是插入模块出错,报错信息如下(这个模块也不能卸载,但重启虚拟机之后可以卸载)

后来经过和实验室师兄探讨,获得了新思路,即当前进程的sibling成员可能并没有指向任何东西,就导致这种错误,故在代码中添加了筛选的条件,让当前进程指向的进程一定得存在,再次编译并插入模块,发现可以正常打印。

4.3、代码实现:4.3.1、第一种思路代码实现

print_bro_child.c

#include <linux/kernel.h>#include <linux/init.h>#include <linux/module.h>#include <linux/sched.h> //task结构体#include <linux/init_task.h> #include <linux/types.h>static int __init print_pcb_info(void) { struct task_struct *task,*p,*child,*bro; struct list_head *pos,*childpos,*brother; //双向链表 //计数器 int process_count=0; printk("progress begin...\n"); task=&init_task; //指向0号进程pcb list_for_each(pos,&task->tasks) { int child_process_count=0,brother_process_count=0; //计数器 p=list_entry(pos,struct task_struct,tasks); //此时的p指针已经指向task_struct结构体的首部,后面就可以通过p指针进行操作 process_count++; printk("第%d个进程信息如下:\n",process_count); printk("name: %s, pid: %d, parent_pid: %d\n",p->comm,p->pid,(p->parent)->pid); printk("--------------------------------子进程信息如下-------------------------------------------\n"); //打印子进程的内容 list_for_each(childpos,&p->children){ child=list_entry(childpos,struct task_struct,sibling); if(child->pid>0){ child_process_count++; printk("进程 %s 的第 %d 个子进程信息:name: %s, pid: %d\n",p->comm,child_process_count,child->comm,child->pid); } } printk("该进程有 %d 个子进程\n",child_process_count); printk("--------------------------------兄弟进程信息如下------------------------------------------\n"); //打印兄弟进程的内容 list_for_each_entry(bro, &(p->parent->children), sibling) { if(bro->pid>0){ brother_process_count++; printk("进程 %s 的第 %d 个兄弟进程信息:name: %s, pid: %d\n",p->comm,brother_process_count,bro->comm,bro->pid); } } printk("该进程有 %d 个兄弟进程\n",brother_process_count); printk("-------------------------------- 此进程信息打印完毕------------------------------------------\n"); printk("\n"); } printk("进程的个数:%d\n",process_count); return 0;} static void __exit exit_pcb_info(void){ printk("goodbye!...\n");} module_init(print_pcb_info);module_exit(exit_pcb_info);MODULE_LICENSE("GPL");

运行结果

在这里,我们使用pstree命令查看一下进程树

用pstree打印进程中systemd->sh->node的信息:

可以看出,它把进程gvfsd-metadata的所有兄弟进程都打印出来了

4.3.2、第二种思路代码实现

print_child_bro.c

#include <linux/kernel.h>#include <linux/init.h>#include <linux/module.h>#include <linux/sched.h> //task结构体#include <linux/init_task.h> #include <linux/types.h>static int __init print_pcb_info(void) { struct task_struct *task,*p,*child,*bro; struct list_head *pos,*childpos,*brother; //双向链表 //计数器 int process_count=0; printk("progress begin...\n"); task=&init_task; //指向0号进程pcb list_for_each(pos,&task->tasks) { int child_process_count=0,brother_process_count=0; //计数器 p=list_entry(pos,struct task_struct,tasks); //此时的p指针已经指向task_struct结构体的首部,后面就可以通过p指针进行操作 process_count++; printk("第%d个进程信息如下:\n",process_count); printk("name: %s, pid: %d, parent_pid: %d\n",p->comm,p->pid,(p->parent)->pid); printk("--------------------------------子进程信息如下-------------------------------------------\n"); //打印子进程的内容 list_for_each(childpos,&p->children){ child=list_entry(childpos,struct task_struct,sibling); if(child->pid>0){ child_process_count++; printk("进程 %s 的第 %d 个子进程信息:name: %s, pid: %d\n",p->comm,child_process_count,child->comm,child->pid); } } printk("该进程有 %d 个子进程\n",child_process_count); printk("--------------------------------兄弟进程信息如下------------------------------------------\n"); //打印兄弟进程的内容 list_for_each_entry(bro, &(p->sibling), sibling) { if(bro->pid>0){ brother_process_count++; printk("进程 %s 的第 %d 个兄弟进程信息:name: %s, pid: %d\n",p->comm,brother_process_count,bro->comm,bro->pid); } } printk("该进程有 %d 个兄弟进程\n",brother_process_count); printk("-------------------------------- 此进程信息打印完毕------------------------------------------\n\n"); } printk("进程的个数:%d\n",process_count); return 0;} static void __exit exit_pcb_info(void){ printk("goodbye!...\n");} module_init(print_pcb_info);module_exit(exit_pcb_info);MODULE_LICENSE("GPL")

Makefile

obj-m +=print_child_bro.oCURRENT_PATH:=$(shell pwd)LINUX_KERNEL:=$(shell uname -r)LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)all: make -C/lib/modules/$(shell uname -r)/build M=$(PWD) modulesclean: make -C/lib/modules/$(shell uname -r)/build M=$(PWD) clean

运行结果:

在这里查看一下systemd->systemd->sh->ibus-daemon这个进程的父进程、子进程及兄弟进程

执行sudo dmesg查看打印的内容,对照上面的进程树,结果正确!

五、打印task_struct字段信息(功能升级):5.1、代码设计思路:

经过前面几次实验之后,对于如何打印task_struct字段、如何传参打印有了一定的掌握,==遂决定探索一下新功能==。本次实验主要是设置了两个内核模块参数,通过传递模块参数processnunm及flag(==其中processnum是特定进程的pid号,flag是自己设置的标志位,0代表打印全部进程信息,1代表打印指定进程的所有子进程的部分信息,2代表打印指定进程的所有兄弟进程的部分信息==)。

关于打印子进程、兄弟进程的原理可参考4.1。

5.2、编写代码:5.2.1、内核模块编写:#include <linux/kernel.h>#include <linux/init.h>#include <linux/module.h>#include <linux/sched.h> //task结构体#include <linux/fdtable.h> //files#include <linux/fs_struct.h> //fs#include <linux/mm_types.h> //打印内存信息#include <linux/init_task.h> #include <linux/types.h>#include <linux/sched.h>#include <linux/atomic.h>#include "policy.h"#include "state.h"MODULE_LICENSE("GPL"); //许可证static int processnum = 156;module_param(processnum, int, 0644);MODULE_PARM_DESC(processnum, "give a pid num to print infomation of it's children/sibling!");static int flag = 0;module_param(flag, int, 0644);MODULE_PARM_DESC(flag, "1 means children and 2 means sibling ");void print(struct task_struct *p ){ printk("pid号: %d\t进程名:%s\t\t状态: %s\t动态优先级: %d\t静态优先级: %d\t实时优先级: %d\t\t父进程pid: %d\tCPU: %d\t调度策略: %s\n",p->pid,p->comm,tran_state(p- >__state),p->prio,p->static_prio,p->rt_priority,(p->parent)- >pid,task_cpu(p),tran_policy(p->policy));}//入口函数static int __init print_pcb(void) //init宏是由init.h文件所支持{ struct task_struct *task,*p,*tmp,*q; struct list_head *pos; //struct task_struct *p = pid_task(find_vpid(processnum),PIDTYPE_PID); //通过find_task_by_vpid(函数找到pid对应进程的task_struct结构体) int count =0; printk("\n-----------------------------------------\n打印进程信息!\n\n"); /*flag==0打印全部进程*/ if(flag==0){ task=&init_task; //指向0号进程pcb printk("全部进程信息:\n"); list_for_each(pos,&task->tasks) { p=list_entry(pos,struct task_struct,tasks); count++; //找到一个进程,自加 print(p); } printk("进程的个数:%d\n",count); return 0; } //找到processnum号对应的task_struct结构体了,在3.1节有分析pid_task,find_get_pid函数 p=pid_task(find_get_pid(processnum),PIDTYPE_PID); print(p); /*flag==1打印进程的子进程*/ if(flag==1){ printk("%d进程%s的全部子进程信息:\n",processnum,p->comm); list_for_each(pos,&p->children) { tmp=list_entry(pos,struct task_struct,sibling); count++; print(tmp); } printk("进程%s子进程个数:%d\n",p->comm,count); } /*flag==2打印进程的兄弟进程*/ else if(flag==2){ printk("%d进程%s的全部兄弟进程信息:\n",processnum,p->comm); q=p; p=p->parent; list_for_each(pos,&p->children) { tmp=list_entry(pos,struct task_struct,sibling); count++; print(tmp); } printk("进程%s的兄弟进程个数:%d\n",q->comm,count); } return 0;} static void __exit exit_pcb(void) //出口函数{ printk("EXIT\n");}// 指明入口点与出口点,入口/出口点是由module.h支持的module_init(print_pcb);module_exit(exit_pcb);5.2.2、调试部分:

将写好的代码文件make编译

在插入模块之前,首先要找到要打印进程的pid号用来传参。为了方便展示,先通过pstree命令来找到有子进程或有兄弟进程的进程,再通过top命令和管道工具找到该进程的进程号。调试部分如下:

找到两个进程systemed、vmware-vmblock-的进程树及其对应的进程号:

插入模块,并传参:

flag置为0,打印所有进程信息:

xhb@xhb-virtual-machine:~/mycode/test8$ sudo insmod pr_task_info.ko flag=0

在打印systemed子进程中我们看到其中一个pid号为485的进程vmware-vmblock-,打印该进程的兄弟进程相关信息:

xhb@xhb-virtual-machine:~/mycode/test8$ sudo insmod pr_task_info.ko processnum=485 flag=2

六、通过eBPF打印task_struct字段:

在经过上述若干个探讨后,越来越发觉task_struct字段的魅力所在,近期又在进行eBPF入门学习,打算通过eBPF对task_struct字段进行打印。

本次实验是对libbpf-bootstrap中example文件夹下的示例程序进行注释、学习、模仿,主要目的是为了学习实践。

6.1、代码更改思路:

对example/c下的bootstrap三个文件进行修改:

在bootstrap.h文件中event结构体进行修改添加;

在bootstrap.bpf.c文件中通过BPF_CORE_READ获取更多关于task_stuct字段中的信息放入event结构体;

6.2、编写代码:

ts_print.bpf.c

// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause/* Copyright (c) 2020 Facebook */#include "vmlinux.h"//用于访问内核数据结构,BPF头文件;#include <bpf/bpf_helpers.h>#include <bpf/bpf_tracing.h>#include <bpf/bpf_core_read.h>#include "ts_print.h"char LICENSE[] SEC("license") = "Dual BSD/GPL";//许可证//两个eBPF maps:exec_start 和 rbstruct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 8192); __type(key, pid_t); __type(value, u64);} exec_start SEC(".maps");//exec_start 是一个哈希类型的 eBPF map,用于存储进程开始执行时的时间戳。struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024);} rb SEC(".maps");//rb 是一个环形缓冲区类型的 eBPF map,用于存储捕获的事件数据,并将其发送到用户态程序。const volatile unsigned long long min_duration_ns = 0;//一个全局变量,最小持续时间/*接下来,我们定义了一个名为 handle_exec 的 eBPF 程序,它会在进程执行 exec() 系统调用时触发。首先,我们从当前进程中获取 PID,记录进程开始执行的时间戳,然后将其存储在 exec_start map 中。*/SEC("tp/sched/sched_process_exec")int handle_exec(struct trace_event_raw_sched_process_exec *ctx){ struct task_struct *task; unsigned fname_off; struct event *e;//struct event结构体被定义在bootstrap.h文件中 pid_t pid; u64 ts; /* remember time exec() was executed for this PID */ /*使用bpf_get_current_pid_tgid()和bpf_ktime_get_ns()函数 将PID和时间戳更新到exec_start BPF映射中,也即上面定义的struct{}exec_start; 来记录该PID的exec()系统调用何时执行。*/ pid = bpf_get_current_pid_tgid() >> 32;//当前进程pid, ts = bpf_ktime_get_ns();//当前时间戳 bpf_map_update_elem(&exec_start, &pid, &ts, BPF_ANY); /* don't emit exec events when minimum duration is specified */ /*检查min_duration_ns是否为非零值。 如果它被设置为非零值,程序会提前退出而不进行进一步处理。 这个条件检查允许应用最短执行时间阈值到执行事件。*/ if (min_duration_ns) return 0; /* reserve sample from BPF ringbuf */ e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);//在rb环形缓冲区中申请空间,类似于malloc(); if (!e)//申请空间失败则退出; return 0; /*然后,我们从环形缓冲区 map rb 中预留一个事件结构, 并填充相关数据,如进程 ID、父进程 ID、进程名等。 之后,我们将这些数据发送到用户态程序进行处理。*/ /* fill out the sample with data */ task = (struct task_struct *)bpf_get_current_task(); e->exit_event = false;//将exit_event设置为false,表示这不是退出事件。 e->pid = pid; e->ppid = BPF_CORE_READ(task, real_parent, tgid);//BPF_CORE_READ获取父进程pid e->__state = BPF_CORE_READ(task, __state);//BPF_CORE_READ获取进程状态; e->prio = BPF_CORE_READ(task, prio);//BPF_CORE_READ获取进程动态优先级; e->static_prio = BPF_CORE_READ(task, static_prio);//BPF_CORE_READ获取进程静态优先级; e->rt_priority = BPF_CORE_READ(task, rt_priority);//BPF_CORE_READ获取进程实时优先级; e->policy = BPF_CORE_READ(task, policy);//BPF_CORE_READ获取进程调度策略; bpf_get_current_comm(&e->comm, sizeof(e->comm));//读取当前命令(进程名称)的名称。 fname_off = ctx->__data_loc_filename & 0xFFFF; bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off); /* successfully submit it to user-space for post-processing */ bpf_ringbuf_submit(e, 0);//将填充的event提交到BPF缓冲区,以供用户空间进行后续处理 return 0;}/*这段代码似乎是BPF跟踪程序的一部分,用于捕获和记录有关进程执行的信息,并将这些数据发送到用户空间的环形缓冲。它还支持事件捕获的最短执行时间阈值。*//*最后,我们定义了一个名为 handle_exit 的 eBPF 程序,它会在进程执行 exit() 系统调用时触发。首先,我们从当前进程中获取 PID 和 TID(线程 ID)。如果 PID 和 TID 不相等,说明这是一个线程退出,我们将忽略此事件。*/SEC("tp/sched/sched_process_exit")int handle_exit(struct trace_event_raw_sched_process_template *ctx){ struct task_struct *task; struct event *e; pid_t pid, tid; u64 id, ts, *start_ts, duration_ns = 0;//ts时间戳,duration_ns进程持续时间; /* get PID and TID of exiting thread/process */ id = bpf_get_current_pid_tgid();//获取当前进程pid,tgid; pid = id >> 32;//id的后32位为pid; tid = (u32)id;//id的前32位为tgid线程id; /* ignore thread exits */ if (pid != tid)//如果进程pid和线程id不相同,说明当前是线程,直接忽略此事件; return 0; /* if we recorded start of the process, calculate lifetime duration */ /*接着,我们查找之前存储在 exec_start map 中的进程开始执行的时间戳。 如果找到了时间戳,我们将计算进程的生命周期(持续时间),然后从 exec_start map 中删除该记录。 如果未找到时间戳且指定了最小持续时间,则直接返回。*/ start_ts = bpf_map_lookup_elem(&exec_start, &pid);//通过pid在map中查找关联的开始时间戳。 if (start_ts)//如果找到了开始时间戳,计算当前时间与开始时间之间的时间差,以获取进程的运行时间(以纳秒为单位)。 duration_ns = bpf_ktime_get_ns() - *start_ts;//bpf_ktime_get_ns()返回自系统启动以来的时间,即当前时间; else if (min_duration_ns)//没找到了开始时间戳,但 min_duration_ns 变量(最小运行时间)已设置,返回,忽略运行时间不足 min_duration_ns 的进程 return 0; bpf_map_delete_elem(&exec_start, &pid);//从 exec_start BPF Map 中删除与当前进程相关的开始时间戳,因为它已经不再需要。 /* if process didn't live long enough, return early */ /*如果 min_duration_ns 设置了,并且进程的运行时间不足 min_duration_ns, 则返回 0,再次忽略不符合要求的进程。*/ if (min_duration_ns && duration_ns < min_duration_ns) return 0; /* reserve sample from BPF ringbuf */ /*然后,我们从环形缓冲区 map rb 中预留一个事件结构,并填充相关数据, 如进程 ID、父进程 ID、进程名、进程持续时间等。 最后,我们将这些数据发送到用户态程序进行处理。*/ e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); if (!e) return 0; /* fill out the sample with data */ task = (struct task_struct *)bpf_get_current_task(); e->exit_event = true; e->duration_ns = duration_ns; e->pid = pid; e->ppid = BPF_CORE_READ(task, real_parent, tgid); e->__state = BPF_CORE_READ(task, __state);//BPF_CORE_READ获取进程状态; e->prio = BPF_CORE_READ(task, prio);//BPF_CORE_READ获取进程动态优先级; e->static_prio = BPF_CORE_READ(task, static_prio);//BPF_CORE_READ获取进程静态优先级; e->rt_priority = BPF_CORE_READ(task, rt_priority);//BPF_CORE_READ获取进程实时优先级; e->policy = BPF_CORE_READ(task, policy);//BPF_CORE_READ获取进程调度策略; e->exit_code = (BPF_CORE_READ(task, exit_code) >> 8) & 0xff;//t_s结构体中, exit_code为int型 bpf_get_current_comm(&e->comm, sizeof(e->comm)); /* send data to user-space for post-processing */ bpf_ringbuf_submit(e, 0);//将填充的event提交到BPF缓冲区,以供用户空间进行后续处理 return 0;}//这段代码是一个用于处理进程退出事件的 BPF 程序。它会跟踪进程的生命周期,并在进程退出时记录相关信息,然后将这些信息传递到用户空间进行后续处理。

ts_print.c

// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)/* Copyright (c) 2020 Facebook *//*这个用户态程序主要用于加载、验证、附加 eBPF 程序,以及接收 eBPF 程序收集的事件数据,并将其打印出来*/#include <argp.h>#include <signal.h>#include <stdio.h>#include <time.h>#include <sys/resource.h>#include <bpf/libbpf.h>#include "ts_print.h"#include "ts_print.skel.h"char *tran_state(volatile long state){ char *s = ; switch (state) { case 0: s = "TASK_RUNNING "; break; case 1: s = "TASK_INTERRUPTIBLE"; break; case 2: s = "TASK_UNINTERRUPTIBLE"; break; case 4: s = "__TASK_STOPPED"; break; case 8: s = "__TASK_TRACED"; break; case 16: s = "EXIT_ZOMBIE"; break; case 32: s = "EXIT_DEAD"; break; case 64: s = "TASK_DEAD"; break; case 128: s = "TASK_WAKEKILL"; break; case 256: s = "TASK_WAKING"; break; case 512: s = "TASK_PARKED"; break; default: s = "unknown "; break; } return s;}char *tran_policy(unsigned int policy){ char *cp; switch (policy) { case 0: cp = "SCHED_NORMAL"; break; case 1: cp = "SCHED_FIFO"; break; case 2: cp = "SCHED_RR "; break; case 3: cp = "SCHED_BATCH"; break; case 5: cp = "SCHED_IDLE"; break; case 0x40000000: cp = "SCHED_RESET_ON_FORK"; break; default: cp = "unknown"; break; } return cp;}/*定义了一个 env 结构,用于存储命令行参数*/static struct env { bool verbose; long min_duration_ms;//最小运行时间,ms级别} env;/*使用argp库设置了一个参数解析器。*/const char *argp_program_version = "ts_print 0.0";//程序版本const char *argp_program_bug_address = "<bpf@vger.kernel.org>";//错误地址/*对程序目的的描述*/const char argp_program_doc[] = "BPF ts_print demo application.\n"//BPF ts_print演示程序 "\n" "It traces process start and exits and shows associated \n"//它跟踪进程的启动和退出,并显示相关的 "information (filename, process duration, PID and PPID, etc).\n"//信息(文件名,进程持续时间,PID和PPID等) "\n" "USAGE: ./ts_print [-d <min-duration-ms>] [-v]\n";//用法:./ts_print [-d <min-duration-ms>] [-v]/*使用 argp 库来解析命令行参数*/static const struct argp_option opts[] = {//定义了两个命令行选项: { "verbose", 'v', , 0, "Verbose debug output" },//-verbose或-v,用于详细的调试输出 { "duration", 'd', "DURATION-MS", 0, "Minimum process duration (ms) to report" },//-duration用于指定要报告的最短进程持续时间 {},};static error_t parse_arg(int key, char *arg, struct argp_state *state)//这个函数是一个参数解析回调函数,它被用于处理命令行参数{ switch (key) { case 'v': env.verbose = true;//上面定义的结构体中的env break; case 'd': errno = 0; env.min_duration_ms = strtol(arg, , 10);// if (errno || env.min_duration_ms <= 0) { fprintf(stderr, "Invalid duration: %s\n", arg); argp_usage(state); } break; case ARGP_KEY_ARG: argp_usage(state); break; default: return ARGP_ERR_UNKNOWN; } return 0;}static const struct argp argp = {//这是一个 struct argp 类型的常量结构,用于定义参数解析的规则和配置。 .options = opts, .parser = parse_arg, .doc = argp_program_doc,};static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)//用于处理打印日志消息,通常是与 libbpf 库相关的消息。{ if (level == LIBBPF_DEBUG && !env.verbose) return 0; return vfprintf(stderr, format, args);//使用 vfprintf 函数将消息输出到标准错误流(stderr),并返回输出的字符数。}static volatile bool exiting = false;//这是一个 volatile 的布尔变量,通常在多线程或者异步信号处理中使用。它表示程序是否正在退出/*这是一个信号处理函数。当程序接收到指定信号(由 sig 参数指定)时,该函数会被调用。在这个函数中,它将 exiting 设置为 true,表示程序即将退出。*/static void sig_handler(int sig){ exiting = true;}/*事件处理函数,接受一个指向事件数据的指针 data,以及事件数据的大小 data_sz*/static int handle_event(void *ctx, void *data, size_t data_sz){ const struct event *e = data;//将 data 强制类型转换为指向struct event结构体的指针,假设事件数据的格式是按照 struct event 结构体来组织的。 struct tm *tm;//用于存储时间信息,struct tm结构体被定义在<ctime> 头文件中,包含年月日时分秒等。 char ts[32];//struct tm time_t t; time(&t);//获取当前时间的时间戳,并将其存储在变量 t 中 tm = localtime(&t);//使用 localtime 函数将时间戳转换为本地时间,并将结果存储在 tm 指针所指向的结构体中 strftime(ts, sizeof(ts), "%H:%M:%S", tm);//使用 strftime 函数将本地时间格式化为小时:分钟:秒的格式,并将结果存储在 ts 字符数组中 //"TIME", "EVENT", "COMM", "PID", "PPID","STATE","Prio","StaticPrio","RTPriority","Policy" if (e->exit_event) {//若进程退出 printf("%-10s %-10s %-18s %-10d %-10d %-18s %-10d %-15d %-15d %-15s\n", ts, "EXIT", e->comm, e->pid, e->ppid,tran_state(e->__state),e->prio,e->static_prio,e->rt_priority,tran_policy(e->policy) );//打印若干信息,时间戳,进程名,退出代码等 if (e->duration_ns)//打印进程运行时间 printf(" (%llums)", e->duration_ns / 1000000); printf("\n"); } else {//进程未退出,即进程正在执行 printf("%-10s %-10s %-18s %-10d %-10d %-18s %-10d %-15d %-15d %-15s\n", ts, "EXEC", e->comm, e->pid, e->ppid,tran_state(e->__state),e->prio,e->static_prio,e->rt_priority,tran_policy(e->policy) );//打印相关信息,包括文件名 } return 0;}int main(int argc, char **argv){ struct ring_buffer *rb = ;//指向环形缓冲区 struct ts_print_bpf *skel;//用于自行加载和运行BPF程序的结构体,由libbpf自动生成并提供与之关联的各种功能接口; int err;//用于存储错误码 /* Parse command line arguments */ err = argp_parse(&argp, argc, argv, 0, , );// 使用 argp_parse 函数解析命令行参数,如果解析出错,将错误码存储在 err 中 if (err) return err; /* Set up libbpf errors and debug info callback */ /*设置 libbpf 的打印回调函数 libbpf_print_fn,以便在需要时输出调试信息和错误信息*/ libbpf_set_print(libbpf_print_fn); /* Cleaner handling of Ctrl-C */ signal(SIGINT, sig_handler);//注册一个信号处理函数 sig_handler,用于处理 Ctrl-C 信号(SIGINT) signal(SIGTERM, sig_handler);//注册一个信号处理函数 sig_handler,用于处理终止信号(SIGTERM) /* Load and verify BPF application */ /*调用 ts_print_bpf__open() 函数, 该函数用于打开和初始化一个 BPF 程序, 返回一个指向 ts_print_bpf 结构体的指针。 如果初始化失败,将 skel 设置为 。*/ skel = ts_print_bpf__open(); if (!skel) {// 检查是否成功初始化 BPF 程序,如果失败,输出错误信息并返回错误码 1 fprintf(stderr, "Failed to open and load BPF skeleton\n"); return 1; } /* Parameterize BPF code with minimum duration parameter */ /*skel指向整个BPF程序,rodata是BPF 程序中的只读数据部分,在这里将BPF程序中的min_duration_ns进行赋值*/ skel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL; /* Load & verify BPF programs */ /*调用 ts_print_bpf__load() 函数, 该函数用于加载和验证 BPF 程序。 如果加载和验证失败,将错误码存储在 err 中。*/ err = ts_print_bpf__load(skel); if (err) { fprintf(stderr, "Failed to load and verify BPF skeleton\n"); goto cleanup;//跳到cleanup } /* Attach tracepoints */ /*调用 ts_print_bpf__attach() 函数, 该函数用于附加 BPF 程序到系统的 tracepoints。 如果附加失败,将错误码存储在 err 中。*/ err = ts_print_bpf__attach(skel); if (err) { fprintf(stderr, "Failed to attach BPF skeleton\n"); goto cleanup; } /* Set up ring buffer polling */ /*创建一个环形缓冲区*/ /*创建一个环形缓冲区 (ring_buffer),并将其关联到 BPF 程序的一个 map。 bpf_map__fd(skel->maps.rb) 用于获取 map 的文件描述符, handle_event 是处理事件的回调函数。如果创建失败,将 err 设置为 -1 并输出错误信息。*/ /*handle_event() 函数会处理从 eBPF 程序收到的事件。 根据事件类型(进程执行或退出),它会提取并打印事件信息, 如时间戳、进程名、进程 ID、父进程 ID、文件名或退出代码等。*/ rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, , ); if (!rb) { err = -1; fprintf(stderr, "Failed to create ring buffer\n"); goto cleanup; } /*实例化了一个环形缓冲区对象,用于处理 BPF 程序中的 rb 映射。 bpf_map__fd(skel->maps.rb)返回了环形缓冲区所对应的BPF map(即BPF映射表)的文件描述符, 作为ring_buffer__new函数的第一个参数。 第二个参数handle_event是一个函数指针,当缓冲区满或出错时将被调用。 第三个和第四个参数分别用于传递上下文信息和附加的参数,可根据实际需求进行修改。*/ /* Process events */ printf("%-10s %-10s %-18s %-10s %-10s %-18s %-10s %-15s %-15s %-15s\n", "TIME", "EVENT", "COMM", "PID", "PPID", "STATE","Prio","StaticPrio","RTPriority","Policy");//打印标题 /*循环内部会调用 ring_buffer__poll() 函数来等待事件的发生, 最多等待 100 毫秒。如果收到 Ctrl-C 信号,将跳出循环。*/ while (!exiting) {//一直循环直到进程退出 err = ring_buffer__poll(rb, 100 /* timeout, ms */); /*ring_buffer__poll函数用于从名为rb的环形缓冲区中获取数据, 它的第二个参数是超时时间,以毫秒为单位。 该函数可能会阻塞等待新数据可用,或者等待指定的超时时间后返回。*/ /* Ctrl-C will cause -EINTR */ if (err == -EINTR) {//发生中断,即使用了Ctrl-C err = 0; break; } if (err < 0) {//表示在调用ring_buffer__poll时出现了错误 printf("Error polling perf buffer: %d\n", err); break; } }cleanup://清理代码块 /* Clean up */ ring_buffer__free(rb);//释放环形缓冲区 ts_print_bpf__destroy(skel);//释放环形缓冲区 return err < 0 ? -err : 0;}

ts_print.h

/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) *//* Copyright (c) 2020 Facebook */#ifndef __BOOTSTRAP_H#define __BOOTSTRAP_H#define TASK_COMM_LEN 16#define MAX_FILENAME_LEN 127struct event { int pid; int ppid; unsigned exit_code; unsigned long long duration_ns; char comm[TASK_COMM_LEN]; char filename[MAX_FILENAME_LEN]; bool exit_event; unsigned int __state; int prio; int static_prio; unsigned int rt_priority; unsigned int policy; //struct event *childen_next,*sibling_next;};#endif /* __BOOTSTRAP_H */

打印结果:

小结

通过这次实验,首先复习了之前学习的链表以及模块传参部分内容,使我们组更加熟练的运用所学知识。其次学习了进程相关的代码,对书写代码方面有很大提升,在传参部分我遇到了问题,用了find__task_by_pid()这个函数后编译的时候发现报错,于是在源码里面搜索这个函数也没找到,然后问了chatgpt,大概的解释就是版本问题或者操作系统,然后chatgpt又推荐了相关函数,我在源码里面找到之后学习了相关函数并应用到代码之中,最终能实现从给定的pid号找到对应的PCB,这次实验收获很大!还有后面对于进程之间的亲属关系,通过徐晗博同学的指导,使得我了解了并能成功打印相关内容!

可以通过 https://chatgpt.rrjike.com 访问ChatGPT。

0 阅读:0

科技布道师

简介:感谢大家的关注