接上一节https://blog.csdn.net/weixin_45730790/article/details/122521234
为了在内核中模拟多任务并发访问共享链表,我们需要完成下面几个任务。
- 首先,需要在内核中建立一个共享链表,并使用自旋锁结构对其进行访问保护
- 利用工作队列机制建立若干个内核线程,每个内核线程都应该对共享链表进行插入/删除操作
- 创建一个内核定时器,并编写其回调函数,使其在到期时能够删除共享链表中的节点
- 在模块卸载函数中实现链表的销毁
这是我们模拟系统调用任务对共享链表的访问
sharelist.c代码如下:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/list.h>
#include <linux/semaphore.h>
#include <linux/sched.h>
#include <linux/timer.h>
#include <linux/spinlock_types.h>
#include <linux/workqueue.h>
#include <linux/slab.h> /*kmalloc的头文件*/
#include <linux/kthread.h>
#include <linux/kallsyms.h>
#define NTHREADS 200 /* 线程数 */
struct my_struct {
struct list_head list;
int id;
int pid;
};
static struct work_struct queue;
static struct timer_list mytimer; /* 用于定时器队列 */
static LIST_HEAD(mine); /* sharelist头 */
static unsigned int list_len = 0;
static DEFINE_SEMAPHORE(sem); /* 内核线程启动器之间进行同步的信号量,4.15内核适用*/
static DEFINE_SPINLOCK(my_lock); /* 保护对链表的操作,4.15内核适用 */
static atomic_t my_count = ATOMIC_INIT(0); /* 以原子方式进行追加 */
static int count = 0;
static int sharelist(void *data);
static void start_kthread(void);
static void kthread_launcher(struct work_struct *q);
/* 内核线程,把节点加到链表 */
static int sharelist(void *data)
{
struct my_struct *p;
if (count++ % 4 == 0)
printk("\n");
spin_lock(&my_lock); /* 添加锁,保护共享资源 */
if (list_len < 50) {
if ((p = kmalloc(sizeof(struct my_struct), GFP_KERNEL)) == NULL)
return -ENOMEM;
p->id = atomic_read(&my_count); /* 原子变量操作 */
atomic_inc(&my_count);
p->pid = current->pid;
list_add(&p->list, &mine); /* 向队列中添加新字节 */
list_len++;
printk("THREAD ADD:%-5d\t", p->id);
}
else { /* 队列超过定长则删除节点 */
struct my_struct *my = NULL;
my = list_entry(mine.prev, struct my_struct, list);
list_del(mine.prev); /* 从队列尾部删除节点 */
list_len--;
printk("THREAD DEL:%-5d\t", my->id);
kfree(my);
}
spin_unlock(&my_lock);
return 0;
}
/* 调用keventd来运行内核线程 */
static void start_kthread(void)
{
down(&sem);
schedule_work(&queue);
}
static void kthread_launcher(struct work_struct *q)
{
kthread_run(sharelist, NULL, "%d", count);
up(&sem);
}
void qt_task(struct timer_list *timer)
{
spin_lock(&my_lock);
if (!list_empty(&mine)) {
struct my_struct *i;
if (count++ % 4 == 0)
printk("\n");
i = list_entry(mine.next, struct my_struct, list); /* 取下一个节点 */
list_del(mine.next); /* 删除节点 */
list_len--;
printk("TIMER DEL:%-5d\t", i->id);
kfree(i);
}
spin_unlock(&my_lock);
mod_timer(timer, jiffies + msecs_to_jiffies(1000));
}
static int share_init(void)
{
int i;
printk(KERN_INFO"share list enter\n");
INIT_WORK(&queue, kthread_launcher);
timer_setup(&mytimer, qt_task, 0);
add_timer(&mytimer);
for (i = 0; i < NTHREADS; i++)
start_kthread();
return 0;
}
static void share_exit(void)
{
struct list_head *n, *p = NULL;
struct my_struct *my = NULL;
printk("\nshare list exit\n");
del_timer(&mytimer);
spin_lock(&my_lock); /* 上锁,以保护临界区 */
list_for_each_safe(p, n, &mine)
{ /* 删除所有节点,销毁链表 */
if (count++ % 4 == 0)
printk("\n");
my = list_entry(p, struct my_struct, list); /* 取下一个节点 */
list_del(p);
printk("SYSCALL DEL: %d\t", my->id);
kfree(my);
}
spin_unlock(&my_lock); /* 开锁 */
printk(KERN_INFO"Over \n");
}
module_init(share_init);
module_exit(share_exit);
MODULE_LICENSE("GPL v2");
对于共享链表,我们利用内核提供的链表结构list_head来创建其节点, (代码16行)这里将内核提供的list_head类型的链表结构,包含到我们定义的共享链表节点的结构体中,就完成了共享链表节点的定义。通过内核list_head建立的链表,可以直接使用内核中的函数对链表进行插入/删除/遍历等操作。
接着,我们需要为链表创建一个头节点mine(代码22行),这里使用LIST_HEAD宏来完成,这里需要注意的是,头节点是一个list_head类型的结构体,而并不是我们所定义的共享链表节点my_struct结构体,操作时需要特别留意
对于自旋锁,我们使用DEFINE_SPINLOCK的宏来声明,并初始一个自旋锁my_lock,这样我们就完成了第一个任务。在内核中建立了一个头节点为mine的共享链表,并为其创建了一个自旋锁my_lock对其进行访问保护。
下面看如何使用工作队列创建内核线程,为了方便起见,这里使用内核工作队列kevent。
(代码20行)第一步需要定义一个work_struct类型的工作queue,紧接着使用DEFINE_SEMAPHORE的宏声明一个信号量sem并将其初始化为1(代码24行),该信号量的作用在后面会说明。
我们再来看看模块加载函数(代码102行),在模块加载函数中,我们用INT_WORK宏来初始化工作queue,并为其指定工作处理函数kthread_launcher,即内核线程启动器。
工作queue初始化完成后,我们只需要在适当的时候将其插入到内核工作队列kevent中,等待其被执行就好了
(代码105行)for循环,NTHREADS是预定义的宏,表示创建内核线程的个数,这里是200,(代码106行)start_kthread函数被循环执行,用来将工作queue插入到内核工作队列中
(代码66~70行)kthread函数,该函数代码只有2行,首先将信号量sem减1(代码68行),如果没被阻塞的话,则执行schedule_work(代码69行),将工作queue插入到内核工作队列中。
(代码72~76行)kthread_launcher工作处理函数,它的代码也只有两行,首先kthread_run创建并唤醒一个内核线程,第一项参数sharelist是一个函数指针,指定该内核线程需要执行的函数,第二项则是指定的参数将被自动化传递给该函数,后面的两项(“%d”,count)则是格式化的为线程命名,类似于printf函数。count是一个全局变量,用来记录内核线程的序号,然后为其命名。线程创建完毕后,将信号量sem加1,
信号量sem在这里有什么用?我们试图将信号量sem取消,执行后会发现模块只能创建一个内核线程,但是我们明明调用了200次schedule_work函数,向工作队列中插入了200个queue工作,其它的199个工作去哪里了呢 ?原因在于我们执行schedule_work函数时,它会检查要插入的工作是否已经在工作队列中,如果是则结束执行,所以在第一个work被执行前,其它199次对同一个work进行调度,都是无效操作。所以这里我们要使用信号量来保证,每次调度工作被执行之后才进行下一次调度,实现线程启动函数之间的同步。
另外,我们还要考虑一个问题,为什么我们要使用工作队列来创建内核线程?而不是直接调用200次kthread_run函数?内核队列keventd_wq默认的工作者线程叫做events/n,这里的n是处理器的编号,每个处理器对应一个线程,比如单处理器的系统只有events/0这样一个工作者线程,而在双处理器的系统中就会多一个events/1线程,所以,如果我们要创建大量的线程,将这一工作分配给多CPU并行执行,无疑会提升效率,当然,前提是你的系统存在多个CPU。如果我们不想使用工作队列来创建线程,那么也就可以不使用信号量sem了。
现在来看看sharelist函数(34行)它是内核线程需要执行的函数,在sharelist中我们完成共享链表节点的插入或删除,在这里注意到,在对共享链表进行操作之前需要使用spin_lock上锁(42行),list_len是一个全局变量(43行),用来记录链表的长度,当链表长度小于50时,内核线程执行节点的插入操作,(46行)一个原子变量my_count用来记录节点的序号,在这里我们创建了一个新的共享链表节点(44行),并按顺序为其赋值(46行),(49行)使用list_add这一函数,将其插入到头节点mine的后面,当链表长度大于50时,线程执行的是删除节点的操作(53行),(56行)使用list_del函数删除头节点的前驱,也就是链表的尾节点。另外(55行)有一个list_entry的宏,它的作用是找到包含链表尾节点的my_sturct结构体的地址,因为我们不止要删除链表的指针,还需要删除共享节点结构体本身,(55行)我们找到这个结构体地址之后,(59行)使用kfree将其销毁,就完成了共享链表节点的删除。
(61行)在操作完成之后,使用spin_unlock进行解锁,保证每次只要一个线程或其它任务能对链表进行访问,到这里我们完成了第二个任务。利用工作队列机制建立了200个内核线程,并且每个内核线程都能对共享链表进行插入或删除的操作。
接下来看看内核定时器的使用(21行),按照上一节提到的使用流程,首先定义一个timer_list类型的定时器mytimer,接着在模块加载函数中将其初始化(103行),这里使用time_setup宏将mytimer初始化,并为其指定回调函数qt_task,紧接着使用add_timer将其激活(104行),这一定时器就可以开始工作了。
回调函数qt_task(78行),在操作共享链表之前都需要进行加锁(80行),(81行)当链表不为空时,我们需要删除(86行)头节点的后续节点,最后(92行)使用mod_timer修改定时器的到期时间,将定时器的下一次到期时间设置为1000毫秒之后。msec_to_jiffies是一个函数,用来将毫秒值转化为节拍数。内核定时器的任务到此结束。
最后的任务是在模块卸载函数中销毁链表,(114行)首先是删除定时器mytimer,同样(115行)在访问链表之前要先上锁,紧接着使用list_for_each_safe的宏,来从头遍历链表,依次删除每一个节点(121行),销毁整个链表(123行)。当我们在用户态调用rmmod命令删除该模块时,是通过系统调用delete_module来实现的,delete_module系统调用会执行我们在模块卸载函数中写入的代码,销毁链表,从而模拟了系统调用任务对共享链表的访问,这就是内核多任务并发实例的主要内容。
剩下的是定义的一些二宏(13行)和变量,包括线程数NTHREADS,链表的长度list_len(23行),原子变量mycount(26行),
Makefile文件如下:
obj-m :=sharelist.o
CURRENT_PATH := $(shell pwd)
LINUX_KERNEL := $(shell uname -r)
LINUX_KERNEL_PATH := /lib/modules/$(shell uname -r)/build
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
@rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions Module.symvers .Makefile.swp modules.order *.o.ur-detected *.o.ur-safe
Make之后,用insmod插入模块
dmesg命令查看执行结果。
可以看到,内核线程按顺序的执行插入或删除操作,并且在定时器到期时,也能正确的删除头节点的后继节点,
接着用rmmod命令将模块删除,再次执行dmesg命令,可以看到链表中剩余的节点也被删掉了。
如果对您有帮助,麻烦点赞、收藏或者关注哦~