在内核模块开发中,也存在线程的概念。和应用程序的线程类似,内核也需要多个线程同时并行地执行,避免可能的阻塞。一旦一个内核线程阻塞,不影响其他进程的工作。所谓“内核线程”,是直接由内核本身启动的进程(内核线程的本质是进程),运行在内核态,它与其他进程”并行”执行。
进程的状态
操作系统相关书籍中,对于进程的管理有一个“三态模型”,分别是“就绪态”、“运行态”、阻塞态。如下图所示:
一旦一个进程被创建后,就进入就绪态;当进程得到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命令后,进程名用方括号“[]”括起来的进程是内核线程。