中断

中断描述符表-IDT

中断描述符表IDT又叫中断向量表,基于保护模式下,当中断发生的时候会利用当前的中断向量号查询中断描述符表,因为中断描述符中记录的是当前中断处理程序的地址信息,就可以跳转到中断的处理程序了,实模式下存储中断处理程序的入口地址的表是中断向量表。
在这里插入图片描述
DPL:段描述符的特权级

Offset:中断函数在段中的偏移地址

P:段是否在内存中的标志

S、0:系统段,1表示非系统段,门结构都是系统段,门描述符的S位都是1表示系统段

TYPE:类型字段

段选择子:段内偏移 指示中断服务程序的地址

中断门

在这里插入图片描述
中断门中包含了中断处理程序所在断的段选择子和段内偏移地址,当以这种方式进入中断处理程序后,处理器清IF标志就是关中断,中断门的DPL=0,所有中断处理程序均由中断门激活并将其全部都限制在内核态。

陷阱门

在这里插入图片描述

陷阱门进入中断后标准寄存器的IF位不会自动置0,陷阱门只允许存在IDT表中。

调用门

又叫系统门是提供给用户进程进入特权级0级的方式,DPL=3,可存在于DPL和GDT表中,系统调用就是通过调用门进入内核的。

全局描述符表GDT

段描述符表项为8个字节,用来描述内存段的属性,段描述符主要用于给处理器提供一个段的位置、界限、访问特权级等信息。

在这里插入图片描述
全局描述符表存放在内存中,可以看作是一个数组里面存放的是段描述符,数组中的每个元素都是8个字节的描述符。

段选择子
16位主要用来确定段描述符。
在这里插入图片描述

RPL:请求特权级别

INDEX:GDT表中索引描述符

如何在保护模式下访问内存地址呢?

前提:选择子为 0x8,将其加载到 ds 寄存器后,访问 ds:0x9 的内存地址。

过程:分析选择子 0x8 ,其中 低 2 位是RPL,其值为 00。第 3 位是 TI,其值 0,表示是在 GDT 中索引段描述符。高 13 位 0x1 在 GDT 中的第 1 个段描述符(GDT 中第 0 个段描述符不可用),用 第 1 个段描述符中的 3 个段基址部分与段内偏移地址 0x9 相加所得的和 X 作为访存地址。

中断处理过程

在这里插入图片描述
中断处理过程分为cpu内外两部分,cpu内部主要是利用中断向量号进行一系列过程最终执行中断处理程序,cpu外部处理任务为外设发起中断请求由芯片接收,当芯片完成处理将中断向量号发送给cpu。

那么处理器是如何根据中断向量号定位中断门描述符的呢?

首先中断向量号是中断描述符的索引,当处理器接收到一个外部中断向量号之后,用此向量号在中断描述符表中查询对应的中断描述符,再去执行中断描述符对应的中断处理程序,中断描述符单位为8个字节,中断向量号乘以8+IDTR表中的中断描述符表地址=中断向量号对应的中断描述符。

处理器如何进行特权级检查?

当用户进程发起int n引发的软中断,处理器要检查当前特权级CPL和门描述符DPL,如果CPL<=DPL数值上,则检查特权级下限通过。

处理器检查CPL和门描述符中所记录的选择子对应的目标代码段DPL,数值上CPL>DPL,检查特权级上限通过。

如果引发的中断是外设或者异常导致,那么需要检查数值上CPL>目标代码段DPL。(权限上相反)

找到中断处理程序后如何执行?

门描述符目标代码选择子加载到代码段寄存器CS中,把门描述符中中断处理程序偏移地址加载到EIP后开始执行中断处理程序。

中断产生,eflags中的NT位和TF位会被置0,门存放于某门描述符。

中断对应的是中断门:标志寄存器eflags中IF位自动置0避免中断嵌套引发异常。

中断对应的是任务门或者陷阱门:IF位不置0,为什么呢?是因为对于陷阱门来说主要作用是进行调试,所以允许中断嵌套;对任务门来说相当于此次执行的是新任务,所以任务应该在开中断的情况下进行,不然就会独占cpu,就会产生多任务变成单任务局面。

当中断处理程序执行完成之后需要返回,如何返回?
执行iret指令从栈顶弹出寄存器cs、ip等,根据当权DPL的变化决定是否要将栈中数据弹出到寄存器ss和esp中,执行完成之后通过iret从栈中恢复eflags的内容。

中断控制器

电子设备或者芯片处理外设发出的中断请求,负责管理和协调硬中断和软中断,并对其进行正确的响应和处理。

中断状态

inactive无效状态
pending有效状态,等待cpu响应中断
activecpu已经响应中断
active and pendingcpu正在响应中断,但此时还继续有中断源发送中断信号给cpu

硬件中断号分配

中断类型中断号范围
软件触发中断SGI0-15
私有外设中断PPI 设备树116-31
共享外设中断SPI 设备树032-1019

SPI是所有内核共享的,SGI以及PPI是每个cpu私有的中断。

中断下半部分——tasklet、workqueue

taskelet机制

工作队列

工作队列的基本原理是在进程上下文中将需要推迟执行的work添加到工作队列中交给内核线程,内核线程会调用这个work的回调函数来完成,所以说工作队列需要重新调度和睡眠,而上次分析的tasklet也是处理中断下文的机制,但是它是不存在睡眠状态的,所以我们可以这样说tasklet用来处理花费时间相对多的事情,工作队列处理相对比较复杂且更加花费时间的事。

工作线程池

用来管理多个工作线程,目前工作线程池的类型有两种,普通优先级BOUND,高优先级UNBOUND,线程池中的线程数量是动态分配的,当工作线程睡眠时候是否需要唤醒更多的工作线程,有需要的话会唤醒同一个工作线程池中处于空闲的工作线程。

相关数据结构

work_struct–工作

struct work_struct {
	atomic_long_t data;//
	struct list_head entry;//将work挂载到其他队列上
	work_func_t func;//work的处理函数
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

work_struct结构体表示一个工作,主要处理工作的函数为func指针。

worker–工作者线程

struct worker {
	union {
		struct list_head	entry;	/* 在空闲状态下在空闲列表中 */
		struct hlist_node	hentry;	/* 在繁忙状态下在繁忙哈希表中 */
	};

	struct work_struct	*current_work;	/* 当前正在处理的工作项 */
	work_func_t		current_func;	/* current_work 的执行函数 */
	struct pool_workqueue	*current_pwq; /* current_work 的所属工作队列 */
	struct list_head	scheduled;	/* 已调度的工作项列表 */
	
	struct task_struct	*task;		/* 工作线程的任务结构 */
	struct worker_pool	*pool;		/* 关联的工作池 */
	struct list_head	node;		/*  pool->workers 上 */
	
	unsigned long		last_active;	/* 上一次活跃的时间戳 */
	unsigned int		flags;		/* 标志位 */
	int			id;		/* 工作线程的 ID */
	
	char			desc[WORKER_DESC_LEN]; /* 用于调试和打印的描述信息 */
	
	struct workqueue_struct	*rescue_wq;	/* 用于救援的工作队列指针 */
	
	work_func_t		last_func;	/* 用于记录工作线程的上一个任务函数 */

};

work运行于内核线程中需要工作者线程处理工作队列的各个工作,用worker结构体描述,其中current_work是正在处理的workcurrent_func是当前正在处理的work回调函数,pool是将所有被调度且正在准备执行的work挂到该链表中,node将该工作线程挂载到worker_pool->workers链表上。

workqueue_struct–工作队列

struct workqueue_struct {
	struct list_head	pwqs;		//pool-workqueue数据结构挂载到该链表上
	struct list_head	list;		//链表节点

	struct mutex		mutex;		/* 保护该工作队列的互斥锁 */
	int			work_color;	
	int			flush_color;	/* 当前正在进行的刷新操作的颜色 */
	atomic_t		nr_pwqs_to_flush; /* 正在进行刷新操作的池工作队列数量 */
	struct wq_flusher	*first_flusher;	/* 第一个刷新操作器 */
	struct list_head	flusher_queue;	/* 等待进行刷新操作的池工作队列链表 */
	struct list_head	flusher_overflow; /* 刷新操作溢出列表 */
	
	struct list_head	maydays;	//rescuer工作线程,内存不够创建新的工作队列会失败,创建工作队列设置标志位为WQ_MEM_RECLAIM那么由rescuer处理
	struct worker		*rescuer;
	
	int			nr_drainers;	/* 正在进行排空操作的池工作队列数量 */
	int			saved_max_active; /* 保存的池工作队列最大活跃度 */
	
	struct workqueue_attrs	*unbound_attrs;	/* 仅用于无绑定工作队列 */
	struct pool_workqueue	*dfl_pwq;	/* 仅用于无绑定工作队列 */

#ifdef CONFIG_SYSFS
	struct wq_device	*wq_dev;	/* 用于sysfs接口 */
#endif
#ifdef CONFIG_LOCKDEP
	struct lockdep_map	lockdep_map;
#endif
	char			name[WQ_NAME_LEN]; /* 工作队列的名称 */

	/*
	 * workqueue_struct的销毁受sched-RCU保护,以允许在不抓取wq_pool_mutex的情况下遍历workqueues列表。
	 * 这用于从sysrq中转储所有workqueues。
	 */
	struct rcu_head		rcu;
	
	/* 热字段,在发出命令时使用,对齐到缓存行 */
	unsigned int		flags ____cacheline_aligned; /* 标志位 */
	struct pool_workqueue __percpu *cpu_pwqs; /* 每个CPU的池工作队列 */
	struct pool_workqueue __rcu *numa_pwq_tbl[]; /* 由节点索引的无绑定池工作队列表 */

};

多个工作组成工作队列,工作队列共享一组工作线程池,当一个work挂载到工作队列中,需要通过工作线程池中的工作线程来处理其回调函数。

pool_workqueue–关联workqueue、worker_pool

struct pool_workqueue {
	struct worker_pool *pool;		/* 关联的工作者池 */
	struct workqueue_struct *wq;	/* 拥有该池工作队列的工作队列 */
	int work_color;					
	int flush_color;				
	int refcnt;						/* 引用计数 */
	int nr_in_flight[WORK_NR_COLORS];
									/* 进行中的工作项数量 */
	int nr_active;					/* 活跃的工作项数量 */
	int max_active;					/* 最大活跃的工作项数量 */
	struct list_head delayed_works; /* 延迟工作项链表 */
	struct list_head pwqs_node;		/* 在wq->pwqs上的节点 */
	struct list_head mayday_node;	/* 在wq->maydays上的节点 */

	/*
	 * 未绑定pwq的释放延迟到system_wq。请参阅put_pwq()和pwq_unbound_release_workfn()以了解详细信息。
	 * pool_workqueue本身也受sched-RCU保护,以便可以在不抓取wq->mutex的情况下确定第一个pwq。
	 */
	struct work_struct unbound_release_work;
	struct rcu_head rcu;

} __aligned(1 << WORK_STRUCT_FLAG_BITS);

关系图
在这里插入图片描述

pool_workqueue充当桥梁的作用将工作集合和工作线程集合进行关联。

如何调度一个work?

内核提供处理的宏

#define __INIT_WORK(_work, _func, _onstack)				\
	do {								\
		__init_work((_work), _onstack);				\
		(_work)->data = (atomic_long_t) WORK_DATA_INIT();	\
		INIT_LIST_HEAD(&(_work)->entry);			\
		(_work)->func = (_func);				\
	} while (0)
#endif

#define INIT_WORK(_work, _func)						\
	__INIT_WORK((_work), (_func), 0)

此时data成员被划分成高位域和低位域,高位域存放work上次执行的worker_pool的id,低位域存放与work相关的标志位。

work标志位如下

enum {
	WORK_STRUCT_PENDING_BIT	= 0,	/* work item is pending execution */
	WORK_STRUCT_DELAYED_BIT	= 1,	/* work item is delayed */
	WORK_STRUCT_PWQ_BIT	= 2,	/* data points to pwq */
	WORK_STRUCT_LINKED_BIT	= 3,	/* next work is linked to this one */
#ifdef CONFIG_DEBUG_OBJECTS_WORK
	WORK_STRUCT_STATIC_BIT	= 4,	/* static initializer (debugobjects) */
	WORK_STRUCT_COLOR_SHIFT	= 5,	/* color for workqueue flushing */
#else
	WORK_STRUCT_COLOR_SHIFT	= 4,	/* color for workqueue flushing */
#endif

	WORK_STRUCT_COLOR_BITS	= 4,

	WORK_STRUCT_PENDING	= 1 << WORK_STRUCT_PENDING_BIT,
	WORK_STRUCT_DELAYED	= 1 << WORK_STRUCT_DELAYED_BIT,
	WORK_STRUCT_PWQ		= 1 << WORK_STRUCT_PWQ_BIT,
	WORK_STRUCT_LINKED	= 1 << WORK_STRUCT_LINKED_BIT,
#ifdef CONFIG_DEBUG_OBJECTS_WORK
	WORK_STRUCT_STATIC	= 1 << WORK_STRUCT_STATIC_BIT,
#else
	WORK_STRUCT_STATIC	= 0,
#endif

	/*
	 * The last color is no color used for works which don't
	 * participate in workqueue flushing.
	 */
	WORK_NR_COLORS		= (1 << WORK_STRUCT_COLOR_BITS) - 1,
	WORK_NO_COLOR		= WORK_NR_COLORS,

	/* not bound to any CPU, prefer the local CPU */
	WORK_CPU_UNBOUND	= NR_CPUS,

	/*
	 * Reserve 7 bits off of pwq pointer w/ debugobjects turned off.
	 * This makes pwqs aligned to 256 bytes and allows 15 workqueue
	 * flush colors.
	 */
	WORK_STRUCT_FLAG_BITS	= WORK_STRUCT_COLOR_SHIFT +
				  WORK_STRUCT_COLOR_BITS,

	/* data contains off-queue information when !WORK_STRUCT_PWQ */
	WORK_OFFQ_FLAG_BASE	= WORK_STRUCT_COLOR_SHIFT,

	__WORK_OFFQ_CANCELING	= WORK_OFFQ_FLAG_BASE,
	WORK_OFFQ_CANCELING	= (1 << __WORK_OFFQ_CANCELING),

	/*
	 * When a work item is off queue, its high bits point to the last
	 * pool it was on.  Cap at 31 bits and use the highest number to
	 * indicate that no pool is associated.
	 */
	WORK_OFFQ_FLAG_BITS	= 1,
	WORK_OFFQ_POOL_SHIFT	= WORK_OFFQ_FLAG_BASE + WORK_OFFQ_FLAG_BITS,
	WORK_OFFQ_LEFT		= BITS_PER_LONG - WORK_OFFQ_POOL_SHIFT,
	WORK_OFFQ_POOL_BITS	= WORK_OFFQ_LEFT <= 31 ? WORK_OFFQ_LEFT : 31,
	WORK_OFFQ_POOL_NONE	= (1LU << WORK_OFFQ_POOL_BITS) - 1,

	/* convenience constants */
	WORK_STRUCT_FLAG_MASK	= (1UL << WORK_STRUCT_FLAG_BITS) - 1,
	WORK_STRUCT_WQ_DATA_MASK = ~WORK_STRUCT_FLAG_MASK,
	WORK_STRUCT_NO_POOL	= (unsigned long)WORK_OFFQ_POOL_NONE << WORK_OFFQ_POOL_SHIFT,

	/* bit mask for work_busy() return values */
	WORK_BUSY_PENDING	= 1 << 0,
	WORK_BUSY_RUNNING	= 1 << 1,

	/* maximum string length for set_worker_desc() */
	WORKER_DESC_LEN		= 24,
};
static inline bool schedule_work(struct work_struct *work)
{
	return queue_work(system_wq, work);
}

初始化完成work调用schedule_work 会将 work 添加到默认的BOUND类型工作队列也就是 system_wq 中,如果需要添加到指定的工作队列,可以调用 queue_work接口,第一个参数就是指定的 workqueue_struct 结构。

static inline bool queue_work(struct workqueue_struct *wq,
			      struct work_struct *work)
{
	return queue_work_on(WORK_CPU_UNBOUND, wq, work);//使用本地cpu,工作队列,工作
}
bool queue_work_on(int cpu, struct workqueue_struct *wq,
		   struct work_struct *work)
{
	bool ret = false;
	unsigned long flags;

	local_irq_save(flags);//禁止本地中断

	if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
		__queue_work(cpu, wq, work);//将工作加入到工作队列
		ret = true;
	}

	local_irq_restore(flags);//开中断
	return ret;
}

如果没有为当前的 work 设置 WORK_STRUCT_PENDING_BIT 标志位,说明当前 work 已经被添加到某个工作队列时,该标志位被置位,在工作队列机制中并不允许同一个 work 同时被加入到一个或多个工作队列中,只有当 work 正在执行或者已经执行完成,才能重新添加该 work 到工作队列中,所以不存在 cpu 并发执行。而softirq 支持多 cpu 的并发执行, tasklet不允许多 cpu 的并发执行。

static void __queue_work(int cpu, struct workqueue_struct *wq,
			 struct work_struct *work)
{
	struct pool_workqueue *pwq;
	struct worker_pool *last_pool;
	struct list_head *worklist;
	unsigned int work_flags;
	unsigned int req_cpu = cpu;

	// 确保在中断被禁用的情况下执行该函数
	lockdep_assert_irqs_disabled();
	
	debug_work_activate(work);
	
	// 销毁工作队列不允许过程中由work加入队列
	if (unlikely(wq->flags & __WQ_DRAINING) &&
	    WARN_ON_ONCE(!is_chained_work(wq)))
		return;

retry:
	if (req_cpu == WORK_CPU_UNBOUND)
		cpu = wq_select_unbound_cpu(raw_smp_processor_id());
		// 根据工作队列的属性选择要使用的工作者池
if (!(wq->flags & WQ_UNBOUND))
	pwq = per_cpu_ptr(wq->cpu_pwqs, cpu);//本地cpu的pool_workqueue
else
	pwq = unbound_pwq_by_node(wq, cpu_to_node(cpu));//本地节点对应的

last_pool = get_work_pool(work);//查询work上一次位于哪个work_pool中运行
if (last_pool && last_pool != pwq->pool) {
	struct worker *worker;

	spin_lock(&last_pool->lock);

	worker = find_worker_executing_work(last_pool, work);//work是否正在某个worker_pool上执行

	if (worker && worker->current_pwq->wq == wq) {
		pwq = worker->current_pwq;
	} else {
		// 如果工作项不在另一个工作者池中运行,将其在当前工作者池中排队
		spin_unlock(&last_pool->lock);
		spin_lock(&pwq->pool->lock);
	}
} else {
	spin_lock(&pwq->pool->lock);
}

// 工作者池已确定并锁定。对于非绑定的工作者池,可能会与工作者池的释放发生竞争,因此可能已经被释放。如果引用计数为零,则重新选择工作者池
if (unlikely(!pwq->refcnt)) {
	if (wq->flags & WQ_UNBOUND) {
		spin_unlock(&pwq->pool->lock);
		cpu_relax();
		goto retry;
	}
	// 出现错误
	WARN_ONCE(true, "workqueue: per-cpu pwq for %s on cpu%d has 0 refcnt",
		  wq->name, cpu);
}

// 将工作项插入工作者池的工作队列中
trace_workqueue_queue_work(req_cpu, pwq, work);

if (WARN_ON(!list_empty(&work->entry))) {
	spin_unlock(&pwq->pool->lock);
	return;
}

// 更新工作者池中的计数和标志
pwq->nr_in_flight[pwq->work_color]++;
work_flags = work_color_to_flags(pwq->work_color);
//活跃的work数,大于则加入等待链表worker_pool->worklist否则加入delayed_works
if (likely(pwq->nr_active < pwq->max_active)) {
	trace_workqueue_activate_work(work);
	pwq->nr_active++;
	worklist = &pwq->pool->worklist;
	if (list_empty(worklist))
		pwq->pool->watchdog_ts = jiffies;
} else {
	work_flags |= WORK_STRUCT_DELAYED;
	worklist = &pwq->delayed_works;
}

insert_work(pwq, work, worklist, work_flags);//添加work到work_pool工作队列中

spin_unlock(&pwq->pool->lock);
}
static void insert_work(struct pool_workqueue *pwq, struct work_struct *work,
            struct list_head *head, unsigned int extra_flags)
{
    struct worker_pool *pool = pwq->pool;
    //设置 work 的 pwq 和 flag。
    set_work_pwq(work, pwq, extra_flags);
    //将 work 添加到 worklist 链表中
    list_add_tail(&work->entry, head);
    //为 pwq 添加引用计数
    get_pwq(pwq);
    //添加内存屏障,防止 cpu 将指令乱序排列
    smp_mb();

    //唤醒 worker 对应的内核线程
    if (__need_more_worker(pool))
        wake_up_worker(pool);

}

通过获取当前cpu的id,通过id获取当前被使用的工作队列所绑定的pool_workqueue,通过中间介质找到worker_pool。

等待work是否执行完成调用flush_work。

bool flush_work(struct work_struct *work)
{
	return __flush_work(work, false);
}

work此时如果并未完成那么将会被阻塞直到work执行完成,如果此时一个work被添加到多个工作队列中,同样需要等待所有的work均已执行完成,返回true,如果进入空闲状态则返回false,存在延迟的work则调用flush_delayed_work。

1 bool flush_delayed_work(struct delayed_work *dwork)
2 {
3     local_irq_disable();   // 禁用本地中断,确保代码执行的原子性和一致性
4     if (del_timer_sync(&dwork->timer))   // 删除定时器并等待其完成
5         __queue_work(dwork->cpu, dwork->wq, &dwork->work);   // 将工作项重新添加到工作队列中
6     local_irq_enable();   // 启用本地中断
7     return flush_work(&dwork->work);   // 刷新工作项队列,返回刷新状态
8 }
bool cancel_work_sync(struct work_struct *work)
{
	return __cancel_work_timer(work, false);
}

或者可以执行取消操作,调用cancel_work_sync直接取消当前指定的work,如果当前work正在处于运行状态,那么该函数同样会阻塞直到该work执行完成,与此同时将会取消当前工作队列中加入的work。

实践

要求:写一个简单的内核模块,初始化一个工作队列,在write()函数里调用该工作队列回调函数,在回调函数中输出用户程序写入的字符串。并写一个测试程序测试该功能。

内核态代码:

在上次tasklet机制上修改代码。

//私有数据结构体
struct mydemo_private_data {
    struct mydemo_device *device;
    char name[64];  
   // struct tasklet_struct tasklet;//tasklet队列

struct work_struct my_work;//工作队列   

};
//打开设备文件
static int demodrv_open(struct inode *inode, struct file *file)
{
    unsigned int minor = iminor(inode);
    struct mydemo_private_data *data;
    struct mydemo_device *device = mydemo_device[minor];//从指针数组中获取对应的设备信息dev_info(device->dev, "%s: major=%d, minor=%d, device=%s\n", __func__,MAJOR(inode->i_rdev), MINOR(inode->i_rdev), device->name);
// 申请一个 mydemo_private_data 结构体类型的空间,用于存储打开设备的私有数据
​    data = kmalloc(sizeof(struct mydemo_private_data), GFP_KERNEL);if (!data)return -ENOMEM;sprintf(data->name, "private_data_%d", minor);// tasklet_init(&data->tasklet, do_tasklet, (unsigned long)device);
    INIT_WORK(&data->my_work, do_work);//初始化工作队列
    //参数:将软中断对象的指针地址存储在设备数据结构体 、软中断被触发后执行的回调函数,将设备指针转换为无符号长整型,并将其作为参数传递给处理函数 do_tasklet()//将设备对象或设备数据的指针存储在文件的私有数据字段
    data->device = device;
​    file->private_data = data;return 0;
}
static ssize_t
demodrv_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    struct mydemo_private_data *data = file->private_data;
    struct mydemo_device *device = data->device;unsigned int actual_write;int ret;if (kfifo_is_full(&device->mydemo_fifo)){if (file->f_flags & O_NONBLOCK)return -EAGAIN;dev_info(device->dev, "%s:%s pid=%d, going to sleep\n", __func__, device->name, current->pid);
​        ret = wait_event_interruptible(device->write_queue,!kfifo_is_full(&device->mydemo_fifo));if (ret)return ret;}mutex_lock(&device->lock);

​    ret = kfifo_from_user(&device->mydemo_fifo, buf, count, &actual_write);if (ret)return -EIO;
   //选在在FIFO缓冲区有空间可写的时候进行触发
	schedule_work(&data->my_work);//触发mutex_unlock(&device->lock);if (!kfifo_is_empty(&device->mydemo_fifo)) {wake_up_interruptible(&device->read_queue);kill_fasync(&device->fasync, SIGIO, POLL_IN);printk("%s kill fasync\n", __func__);}dev_info(device->dev, "%s:%s pid=%d, actual_write =%d, ppos=%lld, ret=%d\n", __func__,
​            device->name, current->pid, actual_write, *ppos, ret);return actual_write;
}

触发的回调函数为my_work,编写my_work函数实现简单的打印。

static void do_work(struct work_struct *work)//回调函数
{
	struct mydemo_private_data *data;
	struct mydemo_device *device;

	data = container_of(work, struct mydemo_private_data, my_work);	

	device = data->device;获取设备指针

	dev_info(device->dev, "%s: trigger a work\n", __func__);
}

container_of宏计算出结构体的在内存中的起始地址并返回。

#define container_of(ptr, type, member) ({ \
    const typeof( ((type *)0)->member ) *__mptr = (ptr); \
    (type *)( (char *)__mptr - offsetof(type,member) );})

type:结构体类型
member:结构体内的成员
ptr:结构体内成员 member 的地址

在这里插入图片描述
拿出该结构体mydemo_private_data的成员my_work的地址,减去my_work在结构体中的偏移计算得到mydemo_private_data的首地址,最终返回计算得到的值。

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

偏移量的计算将0强制转换成一个指向TYPE结构体的指针,通过指针访问成员并获取成员的地址,那就是my_workmydemo_private_data中的地址,它的值其实就是我们要计算的偏移量my_workmydemo_private_data中的偏移。

编译一下

make编译并安装内核模块终端会看到当前已经成功注册了一个字符设备。
在这里插入图片描述
当前字符设备的主设备号是247,ls -l /dev/下查看设备信息并未发现存在主设备号247的设备手动mknod。

mknod /dev/mydemo0 c 247 1

测试一下
在这里插入图片描述
可以看到已经成功运行并打开了编写的字符设备,其主设备号为247,次设备号为1与我们手动创建的一致。

echo命令写入字符串到创建的/dev/mydemo0设备中,发现已经触发了write并写入字符串,触发workqueue的回调函数打印do_work: trigger a work,调用read函数读取到的字符串正是我们写入的。
在这里插入图片描述

总结

workqueue工作流程

(1)workqueue由一个或多个worker线程池组成,每个worker线程都会不断地从workqueue中获取需要执行的工作项。
(2)如果此时有延迟的work,将多个work组装成工作项添加到workqueue工作队列中。
(3)工作者线程worker在空闲状态下可以从workqueue工作队列中取出一个work放在自己的私有队列等待执行。
(4)当工作者线程worker完成当前的工作项,将会处理自己私有队列的下一个工作项。
(5)如若此时workqueue已经不存在可用的工作项了,此时工作者线程会处于等待状态,等待新task加入到workqueue工作队列中。

定时器和内核线程

用定时器模拟中断,并建立一个内核线程,当定时器到来的时候将内核线程唤醒,在内核线程程序中输出内核线程相关信息。

编写初始化模块函数

static int __init my_init(void)
{
	printk(" my lockdep module init\n");  
	thread = kthread_run(my_thread, NULL, "ktest");
	my_timer.expires = jiffies + msecs_to_jiffies(500); 
	add_timer(&my_timer);                               
	init_waitqueue_head(&wait_head); 
 	return 0;
}

初始化的时候kthread_run()创建一个内核线程、设置定时器首次触发时间并将其添加,init_waitqueue_head()对等待队列进行初始化。

msecs_to_jiffies宏将是将毫秒转换成滴答数,它根据系统的HZ值来计算对应的滴答数。当你需要设置一个基于时间的事件时,比如定时器,你需要指定在未来的某个时间点触发该事件,而这个时间点就是用 jiffies 加上一定数量的滴答数来表示的。

#define msecs_to_jiffies(msec) ((msec) * HZ / 1000)

此时计算500毫秒所对应的滴答数,那么就是x=(HZ*500)/1000,HZ值大小取决于系统,计算出来的结果就是设置的定时器,那么定时器首次触发的时间是用jiffies系统启动以来的滴答数+x毫秒,触发my_timefunc()函数。

编写定时处理器的函数

static void my_timefunc(struct timer_list *unused)
{
	atomic_set(&flags, 1);  // 设置 flags 值为1
	wake_up_interruptible(&wait_head);  // 唤醒在等待队列上的线程
	mod_timer(&my_timer, jiffies + msecs_to_jiffies(2000));  // 重设定时器,2秒后再次触发
}

每隔一段时间就会调用wake_up_interruptible()唤醒等待队列wait_head上的线程。

内核线程主要执行的程序

// 内核线程的主要执行函数
static int my_thread(void *nothing)
{
	set_freezable();               // 设置线程响应冻结进程的请求
	set_user_nice(current, 0);     // 设置线程的nice值,设置0则表示使用系统默认的优先级

	while (!kthread_should_stop()) {  // 检查线程是否应该停止
		my_try_to_sleep();             
		atomic_set(&flags, 0);
		show_reg();                    
	}
	return 0;
}

进入while循环后利用my_try_to_sleep()函数尝试将线程置于睡眠状态,直到设定的定时器执行完毕,此时atomic_set()中的flags=0为了防止操作产生的并发问题,show_reg()函数打印当前CPU的一些寄存器信息。

static void my_try_to_sleep(void)
{
	DEFINE_WAIT(wait);  

	if (freezing(current) || kthread_should_stop())
		return;  // 如果当前线程需要被冻结或停止,则直接返回

	prepare_to_wait(&wait_head, &wait, TASK_INTERRUPTIBLE);  // 准备将线程放入等待队列,这个操作并不会导致线程立即睡眠。第三个参数 TASK_INTERRUPTIBLE 表示线程进入可中断的睡眠状态,即线程在收到信号时可以被唤醒。

	if (!atomic_read(&flags))
		schedule();  // 如果 flags 为0,则让线程进入睡眠
	
	finish_wait(&wait_head, &wait);  // 从等待队列中移除线程
}

DEFINE_WAIT()将当前线程添加到等待队列中使其能够被唤醒,真正进入睡眠状态是flag=0条件下在schedule()函数中完成,线程会一直处于睡眠状态,必须等待设置的定时器超时将其唤醒。

static void show_reg(void)
{
	unsigned int spsr, sp;
	struct task_struct *task = current;
	
	asm("mrs %0, spsr_el1" : "=r" (spsr) : : "cc");  // 读取SPSR寄存器的值
	asm("mov %0, sp" : "=r" (sp) : : "cc");           // 读取SP寄存器的值

	printk("%s: %s, pid:%d\n", __func__, task->comm, task->pid);  // 打印线程信息
	printk("cpsr:0x%x, sp:0x%x\n", spsr, sp);                     // 打印寄存器信息
}

模块退出的时候需要停止内核线程。

static void __exit my_exit(void)
{
	printk("goodbye\n");  // 打印退出信息
	kthread_stop(thread);  // 停止内核线程
}

调试运行打印线程的名字是我们设定好的,线程pid为2298,cpsr、sp寄存器在内存中地址。

在这里插入图片描述

CPSR–状态寄存器

在这里插入图片描述

前4位为标记位、低5位为运算位、中间保留位

N[31]:负的条件标记
Z[30]:零的条件标记
C[29]:操作进位
V[28]:操作溢出
E[9]:大小端序的设置
F[8]:快速中断的频闭位
I[7]:置1禁止IRQ中断
F[6]:置1禁止FIQ中断
T[5]:置0ARM执行、置1Thumb执行
M[4-0]:处理器工作模式

sp–堆栈寄存器

在这里插入图片描述

当前要出栈或入栈的数据在操作执行后自动递增或递减,具有后进先出、先进后出特点且保存数据时SP总指向最后一个压入堆栈的数据所在的数据单元栈顶。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值