ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

Linux内核学习9——内核多任务并发实例(下)

2022-01-17 12:30:16  阅读:162  来源: 互联网

标签:struct list 链表 线程 内核 Linux my 多任务


接上一节https://blog.csdn.net/weixin_45730790/article/details/122521234

为了在内核中模拟多任务并发访问共享链表,我们需要完成下面几个任务。

  1. 首先,需要在内核中建立一个共享链表,并使用自旋锁结构对其进行访问保护
  2. 利用工作队列机制建立若干个内核线程,每个内核线程都应该对共享链表进行插入/删除操作
  3. 创建一个内核定时器,并编写其回调函数,使其在到期时能够删除共享链表中的节点
  4. 在模块卸载函数中实现链表的销毁

这是我们模拟系统调用任务对共享链表的访问

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插入模块

img

dmesg命令查看执行结果。

img

可以看到,内核线程按顺序的执行插入或删除操作,并且在定时器到期时,也能正确的删除头节点的后继节点,

接着用rmmod命令将模块删除,再次执行dmesg命令,可以看到链表中剩余的节点也被删掉了。

img

如果对您有帮助,麻烦点赞、收藏或者关注哦~

标签:struct,list,链表,线程,内核,Linux,my,多任务
来源: https://blog.csdn.net/weixin_45730790/article/details/122537278

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有