修改进程信息的内核模块

  1. 背景
  2. 学习
  3. 内核模块
  4. 后记

背景

有时候,某些进程可能会出现一些异常,需要排查定位异常原因。

获取异常进程的信息是比较容易的,ps top这些都可以简单查看,更详细的信息也可以通过kcore来分析。

但是,如果需要做一些实验修改进程来排查异常原因呢,比如需要修改进程的state或者尝试唤醒进程或者其他操作呢?

学习

可以写一个内核模块,通过加载模块+指定pid的方法来对指定的进程进行修改

userspace最容易获取到的进程的特定标识是什么呢?pid

那接下来就需要查看怎么通过pid来获取task_struct

可以看一下include/linux/pid.h中有一些函数声明

//通过pid获取pid struct
extern struct pid *find_get_pid(int nr);
//通过pid struct获取task_struct
extern struct task_struct *get_pid_task(struct pid *pid, enum pid_type);
//通过task_strcut获取pid struct
extern struct pid *get_task_pid(struct task_struct *task, enum pid_type type);

接下来看一下这几个函数的实现:

  • find_get_pid
//find_get_pid的实现
//看一下主要的实现在get_pid(find_vpid(nr))
struct pid *find_get_pid(pid_t nr)
{
	struct pid *pid;

	rcu_read_lock();
	pid = get_pid(find_vpid(nr));
	rcu_read_unlock();

	return pid;
}

struct pid *find_vpid(int nr)
{
	return find_pid_ns(nr, task_active_pid_ns(current));
}
struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
	return idr_find(&ns->idr, nr);
}
void *idr_find(const struct idr *idr, unsigned long id)
{
	return radix_tree_lookup(&idr->idr_rt, id - idr->idr_base);
}


static inline struct pid *get_pid(struct pid *pid)
{
	if (pid)
		refcount_inc(&pid->count);
	return pid;
}

//相关结构体
struct pid_namespace {
	struct idr idr;
	struct rcu_head rcu;
	unsigned int pid_allocated;
  ...
}
struct idr {
	struct radix_tree_root	idr_rt;
	unsigned int		idr_base;
	unsigned int		idr_next;
};

以上可以看到从pid获取pid struct的过程为:获取当前ns(这块就需要注意了,这里直接获取的current的ns,如果是docker之类的有ns隔离的环境,需要确定调用查询pid的进程和要查询的pid所属的进程是不是处于相同的ns中)->获取ns的idr->根据pid号和idr来查找基数(现在应该是使用xarray了)

所以这里我们也能看出,每个ns结构体使用一个基树/xarrya维护着所有的pid->pid struct的映射

噢,btw,看了一下,IDA和IDR可不只是用于namespace的pid管理呢?这俩还算一个比较高等级的抽象呢,可以用于很多需要分配id/id到指针映射的场景呢。其中IDR用于维护一个ID到指针的映射,IDA只用于分配ID

牛的牛的,之前没注意这块,后面还得单开一篇文章看看这块内容

注意,这里的get_pid对pid struct增加引用计数了,因此,使用完后还需要释放引用计数,使用put_pid

  • get_pid_task
struct task_struct *get_pid_task(struct pid *pid, enum pid_type type)
{
	struct task_struct *result;
	rcu_read_lock();
	result = pid_task(pid, type);
	if (result)
		get_task_struct(result);
	rcu_read_unlock();
	return result;
}

struct task_struct *pid_task(struct pid *pid, enum pid_type type)
{
	struct task_struct *result = NULL;
	if (pid) {
		struct hlist_node *first;
		first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),
					      lockdep_tasklist_lock_is_held());
		if (first)
			result = hlist_entry(first, struct task_struct, pid_links[(type)]);
	}
	return result;
}

static inline struct task_struct *get_task_struct(struct task_struct *t)
{
	refcount_inc(&t->usage);
	return t;
}

//相关结构体
struct pid
{
	refcount_t count;
	unsigned int level;
	spinlock_t lock;
	/* lists of tasks that use this pid */
	struct hlist_head tasks[PIDTYPE_MAX];
	struct hlist_head inodes;
	/* wait queue for pidfd notifications */
	wait_queue_head_t wait_pidfd;
	struct rcu_head rcu;
	struct upid numbers[];
};

可以看到,其实get_pid_task就是通过去遍历查找pid struct中的tasks链表来查找符合条件的任务

注意这里引用计数也增加了,所以不用的时候应该释放掉,通过put_task_struct

不过这块有些疑问,写在后面

  • get_task_pid
struct pid *get_task_pid(struct task_struct *task, enum pid_type type)
{
	struct pid *pid;
	rcu_read_lock();
	pid = get_pid(rcu_dereference(*task_pid_ptr(task, type)));
	rcu_read_unlock();
	return pid;
}

static struct pid **task_pid_ptr(struct task_struct *task, enum pid_type type)
{
	return (type == PIDTYPE_PID) ?
		&task->thread_pid :
		&task->signal->pids[type];
}

类似的,在task_struct中存着pid struct相关的信息呢,获取到这个这个之后再执行一下get_pid就好了

内核模块

通过以上学习,已经知道了pid号大概是怎么和task_struct相互转换的,所以剩下的就是转换成代码就好了

这里提供个大概框架

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/pid.h>
#include <linux/sched.h>
#include <linux/sched/task.h>

static int target_pid = -1;
module_param(target_pid, int, 0);

static int __init XXX(void)
{
        struct task_struct *t;
        struct pid *p;

        if (target_pid < 0) {
                pr_err("target pid < 0\n");
                return -EINVAL;
        }

        p = find_get_pid(target_pid);
        if (!p) {
                pr_err("get pid failed\n");
                return -EINVAL;
        }

        t = get_pid_task(p, PIDTYPE_PID);
        if (!t) {
                pr_err("get task_struct failed\n");
                put_pid(p);
                return -EINVAL;
        }
        
        /*
         *这里可以添加对task_struct的修改内容
         *比如修改状态、唤醒进程之类的操作
         */

        put_pid(p);
        put_task_struct(t);
        return -EINVAL;

}

module_init(XXX);

MODULE_LICENSE("GPL");

后记

  1. 再次注意,使用get_XXX等函数的时候,需要注意下是不是增加了引用计数,等不再使用的时候需要释放引用计数。pid struct、task_struct还有之前遇到的net_device、gendisk这些都是一样的。
  2. 为什么struct pid中的task不直接做成一个task_struct *类型而是搞一个链表呢?可能是为了进程组、多线程考虑?
  3. 接上面2,那如果在一个struct pid中有一个task链表中有几个相同的pid_type的任务,get_pid_task会返回什么呢?应该是返回的链表的第一个task。是否会出现一个task链表中有几个相同的pid_type的任务的情况?以及get_pid_task直接返回第一个task是否合理?

​ 这个后续还得继续研究一下


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 857879363@qq.com