Linux内核线程

本文详细解释了内核模块中的线程概念,介绍了进程的三态和Linux七态模型,以及如何通过kthread_create创建和管理内核线程,涉及状态转换、唤醒机制和示例代码实现。
摘要由CSDN通过智能技术生成

在内核模块开发中,也存在线程的概念。和应用程序的线程类似,内核也需要多个线程同时并行地执行,避免可能的阻塞。一旦一个内核线程阻塞,不影响其他进程的工作。所谓“内核线程”,是直接由内核本身启动的进程(内核线程的本质是进程),运行在内核态,它与其他进程”并行”执行。

进程的状态

操作系统相关书籍中,对于进程的管理有一个“三态模型”,分别是“就绪态”、“运行态”、阻塞态。如下图所示:
在这里插入图片描述
一旦一个进程被创建后,就进入就绪态;当进程得到CPU后,在CPU上运行,进入运行态;在运行过程中,如果需要等待某些资源,则会进入阻塞态;获取到等待的资源后,进程会进入就绪态,等待被调度执行;如果一个进程在运行态,它也可以主动放弃CPU进入就绪态,等待下一次被调度执行。
Linux对于进程管理采用的是“七态模型”,进程有七种状态:运行态(TASK_RUNNING)、可中断睡眠态(TASK_INTERRUPTIBLE)、不可中断睡眠态(TASK_UNINTERRUPTIBLE)、停止态(TASK_STOPPED),跟踪态(TASK_TRACED)、僵死态(TASK_ZOMBIE)、死亡态(TASK_DEAD)。如下图所示:

在这里插入图片描述
由于跟踪态和停止态的状态相似,死亡态和僵死态的状态相似,为了简化起见,下面的描述略去跟踪态和死亡态。
Linux的进程被创建后,会进入运行态;进程运行过程中,如果需要等待某些资源,会进入可中断睡眠态或不可中断睡眠态;进程在运行态收到SIGSTOP信号,会进入停止态;进程即将退出时,会进入僵死态;在不可中断睡眠态获取到了等待的资源,会进入运行态等待调度执行;在可中断睡眠态获取到了等待的资源或是被信号唤醒,会进入运行态等待调度执行;在停止态收到SIGCONT信号后,会重新进入运行态;进程在运行态时也可以主动放弃CPU,这时进程会被重新放入调度队列中等待下一次执行,在调度队列中的进程也处于运行态。这几种状态的意义如下:

状态备注
运行态/TASK_RUNNING包括两种类型的进程,一个是正在运行的进程,一个是可以被运行但是没有被调度的进程。
可中断睡眠态/TASK_INTERRUPTIBLE等待某些资源,该状态能够被信号唤醒
不可中断睡眠态/TASK_UNINTERRUPTIBLE等待某些资源,该状态不能被信号唤醒
停止态/TASK_STOPPED收到SIGSTOP信号后进入该状态,进程暂停运行;该状态下进程收到SIG_CONT后进程继续运行。
跟踪态/TASK_TRACED进程被跟踪时处于的状态,例如:用gdb调试。
僵死态/TASK_ZOMBIE父进程存在而子进程退出时子进程会处于僵尸状态。
死亡态/TASK_DEAD进程彻底消亡前处于的状态。子进程会处于僵尸状态时,父进程调用了wait/waitpid,子进程就会死亡。

进程的这些状态定义于内核源码的include/linux/sched.h头文件中,如下所示:

#define TASK_RUNNING				0x0000  //运行态
#define TASK_INTERRUPTIBLE		0x0001  //可中断睡眠态
#define TASK_UNINTERRUPTIBLE		0x0002  //不可中断睡眠态
#define __TASK_STOPPED			0x0004  //停止态
#define __TASK_TRACED			    0x0008  //跟踪态
……

通过ps -aux命令可以查看进程的状态:
在这里插入图片描述
图中的STAT这一列是进程状态,可能的状态如下:

状态备注
R运行态,正在运行,或在调度队列中的进程
S可中断睡眠态
D不可中断睡眠态
T停止态
t跟踪态
Z僵死态
X死亡态

Linux进程的状态存放于进程控制块中,进程控制块包含了进程的状态、标识、优先级、进程间关系、堆栈和程序信息等。是进程调度的基本单位。其结构体struct task_struct定义于内核源码的include/linux/sched.h头文件中,如下所示:

struct    task_struct{
	volatile long state; 	           //进程状态
    ……
struct mm_struct		*mm;  //进程的内存分布 
  	...... 
	pid_t   pid;			       //进程的pid 
    pid_t   tgid;                //线程组id,对应线程组组长进程的pid...
    struct task_struct __rcu	*real_parent;  //真正的父进程
	struct task_struct __rcu	*parent;      //父进程,进程在被调试时该变量指向调试进程
	struct list_head		children;      //子进程链表
	struct list_head		sibling;       //兄弟进程链表
	struct task_struct		*group_leader;//线程组组长
    ……
	char comm[TASK_COMM_LEN];       //进程的名称
……
struct fs_struct		*fs;          //文件系统信息
struct files_struct		*files;        //当前进程打开的所有文件
……
}

创建内核线程

Linux提供了创建内核线程的接口,该接口在内核源码的include/linux/kthread.h头文件声明(使用该接口需要引入#include <linux/kthread.h>),如下所示:

  • kthread_create(threadfn, data, namefmt, arg…):这是一个可变参数的宏定义,第一个参数threadfn是内核线程的执行函数,原型为int (*threadfn)(void *data),创建的内核线程将执行这个函数。第二个参数data是内核线程的参数,即传入threadfn函数的参数。再后面的参数namefmt,arg进程(内核线程)的名称,创建内核线程后可通过ps命令查看到进程名称。接口的返回值类型是进程控制块指针struct task_struct *,代表创建的内核线程。该接口常见的调用方式为:struct task_struct *x = kthread_create(my_function, NULL, “kmy_thread”),这里传入的第一个参数是自定义的函数,将在内核线程中执行,第二个参数NULL表示my_function函数的参数是空指针,第三个参数表示内核线程的名称是“kmy_thread”。
    通过kthread_create创建内核线程有几点需要注意:
    一、通过kthread_create创建的内核线程状态是TASK_UNINTERRUPTIBLE,是睡眠状态,需要通过接口wake_up_process唤醒后才能运行。
    二、如果内核线程内部是无限循环,在不需要进行内核线程处理的时候,内核线程需要主动放弃CPU的使用权(通过schedule系列函数来进行进程调度)。
    三、如果内核线程内部是循环操作,可以通过kthread_stop接口来设置停止标志,然后用kthread_should_stop接口来判断是否已经设置停止标志。使用这两个接口的作用是在适当的时候让内核线程终止运行。
    关于上面的描述提到的wake_up_process、schedule系列函数、kthread_stop等几个接口声明如下:
  • int kthread_stop(struct task_struct *k):设置进程停止标志,参数k是将要停止进程的进程控制块。
  • bool kthread_should_stop(void):判断当前内核线程是否停止,返回true表示已经设置进程停止标志,该接口一般和kthread_stop函数配合使用。
  • set_current_state(state_value):设置当前进程状态。
  • long schedule_timeout(long timeout):让当前进程睡眠timeout时间后再次接受调度,timeout参数的单位一般是毫秒。
  • int wake_up_process(struct task_struct *p):唤醒某个进程,参数p是将要唤醒的进程的进程控制块。
    下面将实现一个示例程序test_kthread.c,该程序的作用是创建一个内核线程,内核线程每三秒周期打印字符串“hello,kernel thread”。源码如下:
#include <linux/module.h>
#include <linux/kthread.h>                     //创建内核线程需要引入该头文件

static struct  task_struct  *my_task = NULL;     //将保存内核线程的进程控制块 
//内核线程执行函数
static int my_thread(void *thread_param)
{
	while(!kthread_should_stop())             //通过kthread_should_stop判断是否设置进程停止标志
	{
		printk("hello,kernel thread\n");        //打印字符串
		set_current_state(TASK_INTERRUPTIBLE);//设置当前进程状态为可中断睡眠态
		schedule_timeout(3000);              //让当前进程睡眠3秒后再次接受调度执行
	}
	return 0;
}
//加载函数
static int test_kthread_init(void)
{
	my_task = kthread_create(my_thread, NULL, "kmythread");//创建内核线程,线程名称为kmythread
	if(!IS_ERR(my_task))                     //IS_ERR用于判断内核线程是否创建成功
	{
		wake_up_process(my_task);          //通过wake_up_process接口唤醒创建的线程
	}
	return 0;
}
//卸载函数
static void test_kthread_exit(void)
{
	kthread_stop(my_task);                 //通过kthread_stop设置线程停止标志
}
module_init(test_kthread_init);
module_exit(test_kthread_exit);

源码在加载函数test_kthread_init中通过kthread_create接口创建内核线程,传入的第一个参数my_thread是线程的执行函数,第三个参数“kmythread”是内核线程的名称。返回值存放在my_task变量中,my_task就是创建的内核线程的进程控制块。接下来通过IS_ERR判断内核线程是否创建成功,其参数就是进程控制块,如果IS_ERR返回值为0表示创建成功,然后通过wake_up_process来唤醒创建的内核线程,唤醒后,内核线程的执行函数my_thread将得到执行。
源码实现的内核线程执行函数my_thread用于周期打印字符串“hello,kernel thread”。在该函数中,首先通过kthread_should_stop判断当前进程是否已设置停止标志,如果返回值是false,表示没有设置停止标志,则会通过printk打印字符串。然后设置当前进程的状态为TASK_INTERRUPTIBLE(可中断睡眠态),进程将主动放弃CPU进入休眠,通过schedule_timeout设置休眠时间为3秒,3秒后,进程将再次进入运行态,然后再次打印字符串。
在卸载函数中,通过kthread_stop设置内核线程的停止标志,设置该标志后,my_thread函数的while判断将返回true,此时my_thread函数将停止执行。
编译、加载该模块后,多次执行dmesg -c命令,将看到字符串“hello,kernel thread”会周期打印,间隔时间为3秒,如下图所示:
在这里插入图片描述
此时可以通过ps命令看到内核线程,因为创建的内核线程的名称是“kmythread”,执行ps -aux命令后会看到该进程,如下图所示:
在这里插入图片描述
需要注意的是,执行ps命令后,进程名用方括号“[]”括起来的进程是内核线程。

  • 54
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值