一、通过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_start
void *stack;
refcount_t usage;
/* Per task flags (PF_*), defined further below: */
unsigned int flags;
unsigned int ptrace
int 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; //进程pid
pid_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_works
struct 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.o
CURRENT_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) modules
clean:
make -C/lib/modules/$(shell uname -r)/build M=$(PWD) clean
prmod.c
//入口函数
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)是取到哈希表的头指针的头节点
//通过member来获取它对应的type的指针
==综上所述,通过find_get_pid获取指定pid的pid结构体指针,再通过pid_task查找哈希表,并返回对应的PCB,然后就可以访问该进程的一切信息啦==
3.2、编写代码(有传参):
code8.c
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.o
CURRENT_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) modules
clean:
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
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
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.o
CURRENT_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) modules
clean:
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、内核模块编写:
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 */
char LICENSE[] SEC("license") = "Dual BSD/GPL";//许可证
//两个eBPF maps:exec_start 和 rb
struct {
__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 程序收集的事件数据,并将其打印出来*/
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 = "
" ;//错误地址/*对程序目的的描述*/
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
] [-v]\n" ;//用法:./ts_print [-d] [-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结构体被定义在
头文件中,包含年月日时分秒等。 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 */
struct 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;
};
打印结果:
小结
通过这次实验,首先复习了之前学习的链表以及模块传参部分内容,使我们组更加熟练的运用所学知识。其次学习了进程相关的代码,对书写代码方面有很大提升,在传参部分我遇到了问题,用了find__task_by_pid()这个函数后编译的时候发现报错,于是在源码里面搜索这个函数也没找到,然后问了chatgpt,大概的解释就是版本问题或者操作系统,然后chatgpt又推荐了相关函数,我在源码里面找到之后学习了相关函数并应用到代码之中,最终能实现从给定的pid号找到对应的PCB,这次实验收获很大!还有后面对于进程之间的亲属关系,通过徐晗博同学的指导,使得我了解了并能成功打印相关内容!
可以通过 https://chatgpt.rrjike.com 访问ChatGPT。