接上一节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命令,可以看到链表中剩余的节点也被删掉了。
如果对您有帮助,麻烦点赞、收藏或者关注哦~