【嵌入式Linux学习笔记】进阶使用方法

文章详细介绍了Linux系统中用于并发控制的机制,如原子操作、自旋锁、读写锁、顺序锁、信号量、互斥体,并探讨了中断处理,包括中断的上半部和下半部、软中断、tasklet和工作队列。此外,还讨论了阻塞和非阻塞IO以及轮询机制,如select、poll和epoll,最后提到了异步通知和信号处理在驱动程序和应用程序中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述

在正常驱动Linux的外设之后,整个架构已经搭建完成了,接下来要做的就是简化整个操作流程,以及学习Linux的特性,比如并发,阻塞等状态,这样子才能保证整个系统稳定工作

学习视频地址:【正点原子】STM32MP157开发板

并发与竞争

1. 原子操作

定义:不可进一步分割的操作,用于变量或者位操作。

原子操作API函数

定义变量

atomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0

整形操作
在这里插入图片描述
位操作
在这里插入图片描述在这里插入图片描述

2. 自旋锁

定义:保证某个共享资源某一时刻只被一个线程访问,其他想问访问的线程会原地等待,也就是自旋。

API函数

在这里插入图片描述
在中断中使用自旋锁时,获取锁前需要先禁用本地的中断。
在这里插入图片描述
示例
在这里插入图片描述

DEFINE_SPINLOCK(lock) /* 定义并初始化一个锁 */
 
/* 线程 A */
void functionA (){
	unsigned long flags; /* 中断状态 */
	spin_lock_irqsave(&lock, flags) /* 获取锁,并禁止本地中断 */
	/* 临界区 */
	spin_unlock_irqrestore(&lock, flags) /* 释放锁,并禁止本地中断 */
}

/* 中断服务函数 */
void irq() {
	spin_lock(&lock) /* 获取锁 */
	/* 临界区 */
	spin_unlock(&lock) /* 释放锁 */
}

由自旋锁可以衍生出来以下的读写锁和顺序锁。

读写锁

读写自旋锁为读和写操作提供了不同的锁
一次只能允许一个写操作,也就是只能一个线程持有写锁,而且不能进行读操作。
当没有写操作的时候允许一个或多个线程持有读锁,可以进行并发的读操作

typedef struct {
arch_rwlock_t raw_lock;
} rwlock_t;

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

顺序锁

作用:允许同时读写,但只允许一个写的线程。

typedef struct {
 struct seqcount seqcount;
 spinlock_t lock;
} seqlock_t;

在这里插入图片描述

3. 信号量

作用:信号量比自旋锁的优势在于,它会使线程进入休眠状态,切换到其他线程,从而不影响CPU运行效率。这种方式适合于资源占用比较久的场景。

信号量设置值大于1时为计数型信号量,不可用于互斥访问,因为它允许多个线程同时访问共享资源。
信号量设置值为1时为二值信号量,适合于互斥访问。

struct semaphore {
 raw_spinlock_t lock;
 unsigned int count;
 struct list_head wait_list;
};

在这里插入图片描述示例

struct semaphore sem; 	/* 定义信号量 */
sema_init(&sem, 1); 	/* 初始化信号量 */
down(&sem); 			/* 申请信号量 */
/* 临界区 */
up(&sem); 				/* 释放信号量 */

4. 互斥体

作用:与二值信号量的作用一样,但是机制更为专业,在编写 Linux 驱动的时候遇到需要互斥访问的地方建议使用互斥体-mutex。

struct mutex {
atomic_long_t owner;
spinlock_t wait_lock;
};

在这里插入图片描述
示例

struct mutex lock; 		/* 定义一个互斥体 */
mutex_init(&lock); 		/* 初始化互斥体 */
mutex_lock(&lock); 		/* 上锁 */
/* 临界区 */
mutex_unlock(&lock); 	/* 解锁 */

内核定时器

Linux中很多地方都要用到定时器,系统的节拍率可以自行设置。可以通过图形化界面对Linux内核参数进行设置

-> Kernel Features
	-> Timer frequency (<choice> [=y])

在这里插入图片描述

相关API函数

获取当前系统节拍数,可以读取全局变量jiffies
获取每秒节拍数,可以读取全局变量HZ

定时器绕回处理函数
在这里插入图片描述节拍数与一般时间度量转换函数
在这里插入图片描述定时器操作函数

// 初始化 timer_list 类型变量
void timer_setup(struct timer_list *timer, void (*func)(struct timer_list *), unsigned int flags)
// 向 Linux 内核注册定时器,注册后定时器就会运行
void add_timer(struct timer_list *timer)
// 删除定时器
int del_timer(struct timer_list * timer)
// 删除定时器同步版,会等待其他处理器使用完定时器再删除
int del_timer_sync(struct timer_list *timer)
// 修改定时值,定时器如果没有激活的话会被激活
int mod_timer(struct timer_list *timer, unsigned long expires)

延时函数
在这里插入图片描述

中断

使用方式

中断申请
在这里插入图片描述在这里插入图片描述
中断释放
在这里插入图片描述中断处理
在这里插入图片描述中断使能
在这里插入图片描述在这里插入图片描述

中断的上半部与下半部

定义:因为中断需要快进快出,所以其被分成了上半部和下半部,上半部就是上面的中断处理函数,只要中断触发就会执行。下半部就是一些比较耗时的处理过程。Linux提供了几种下半部的机制。

软中断

软中断类型
在这里插入图片描述
操作API函数

// 开启软中断,nr为上述类型之一,action为回调函数
void open_softirq(int nr, void (*action)(struct softirq_action *)) 
// 触发软中断
void raise_softirq(unsigned int nr)

tasklet

tasklet_struct 结构体

struct tasklet_struct
{
 struct tasklet_struct *next; 	/* 下一个 tasklet */
 unsigned long state; 			/* tasklet 状态 */
 atomic_t count;				/* 计数器,记录对 tasklet 的引用数 */
 void (*func)(unsigned long); 	/* tasklet 执行的函数 */
 unsigned long data; 			/* 函数 func 的参数 */
};

初始化

// 初始化tasklet结构体
void tasklet_init(struct tasklet_struct *t,
		void (*func)(unsigned long),unsigned long data);
// 定义 + 初始化
DECLARE_TASKLET(name, func, data)

函数调度,在上半部中使用tasklet_schedule即可使tasklet在合适的时间运行

void tasklet_schedule(struct tasklet_struct *t)

工作队列

定位:工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重
新调度。因此如果要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。

struct work_struct {
 atomic_long_t data; 
 struct list_head entry;
 work_func_t func; /* 工作队列处理函数 */
};

API函数

// 初始化工作
#define INIT_WORK(_work, _func)
// 定义 + 初始化工作 
#define DECLARE_WORK(n, f)
// 工作调度
bool schedule_work(struct work_struct *work)

应用示例

/* 定义工作(work) */
struct work_struct testwork;

/* work 处理函数 */
void testwork_func_t(struct work_struct *work);
{
 /* work 具体处理内容 */
}

/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
 ......
 /* 调度 work */
 schedule_work(&testwork);
 ......
}

/* 驱动入口函数 */
static int __init xxxx_init(void)
{
 ......
 /* 初始化 work */
 INIT_WORK(&testwork, testwork_func_t);
 /* 注册中断处理函数 */
 request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
 ......
}

设备树中断信息节点

GIC中断控制器

控制器节点
在这里插入图片描述
SPI应用节点
在这里插入图片描述

EXTI中断控制器

控制器节点
在这里插入图片描述
在pinctrl节点中定义了GPIO中断控制器,与EXTI控制器联系在一起。
在这里插入图片描述
应用节点

在这里插入图片描述

两种写法

# 一条语句描述中断信息
interrupts-extended = <&gpioa 13 IRQ_TYPE_EDGE_FALLING>;
# 两条语句描述中断信息
interrupt-parent = <&gpioa>;
interrupts = <13 IRQ_TYPE_EDGE_FALLING>;

API接口
在这里插入图片描述
在这里插入图片描述

// 通过设备号获取中断号
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)

// 通过GPIO获取中断号
int gpio_to_irq(unsigned int gpio)

阻塞和非阻塞IO

作用:我们读取外设数据不可while循环一直读取,这样子CPU负载会过大,需要结合阻塞和非阻塞的IO读取方式进行数据读取。

阻塞IO访问

在这里插入图片描述
应用源码

int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR); 		/* 阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); 	/* 读取数据 */

等待队列

作用:在阻塞访问的状态下,设备文件不可用时进程可以进入休眠状态,等待队列用于唤醒进程。

API函数

// 初始化等待队列头
void init_waitqueue_head(struct wait_queue_head *wq_head)
// 定义并初始化等待队列项
DECLARE_WAITQUEUE(name, tsk)
// 添加 / 删除等待队列项
void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
void remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
// 等待唤醒
void wake_up(struct wait_queue_head *wq_head)
void wake_up_interruptible(struct wait_queue_head *wq_head)

等待事件
不同于上面的主动唤醒,可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程。
在这里插入图片描述

非阻塞IO访问

在这里插入图片描述
应用源码

int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); 		/* 阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); 	/* 读取数据 */

轮询操作

select

函数原型

int select(int nfds, 
	fd_set *readfds, 
	fd_set *writefds,
	fd_set *exceptfds, 
	struct timeval *timeout)

nfds:所要监视的这三类文件描述集合中,最大文件描述符加1。
readfds、writefds 和 exceptfds:这三个指针指向描述符集合,readfds 用于监视指定描述符集的读变化,writefs 用于监视这些文件是否可以进行写操作。exceptfds 用于监视这些文件的异常。
timeout:超时时间

示例代码

void main(void)
{
	int ret, fd; /* 要监视的文件描述符 */
	fd_set readfds; /* 读操作文件描述符集 */
	struct timeval timeout; /* 超时结构体 */
	
	fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
	
	FD_ZERO(&readfds); 		/* 清除 readfds */
	FD_SET(fd, &readfds); 	/* 将 fd 添加到 readfds 里面 */
	 
	/* 构造超时时间 */
	timeout.tv_sec = 0;
	timeout.tv_usec = 500000; /* 500ms */
	 
	ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
	switch (ret) {
		case 0: /* 超时 */
			printf("timeout!\r\n");
			break;
		 case -1: /* 错误 */
			printf("error!\r\n");
			break;
		default: /* 可以读取数据 */
			if(FD_ISSET(fd, &readfds)) { /* 判断是否为 fd 文件描述符 */
			/* 使用 read 函数读取数据 */
	}
	break;
	} 
}
poll

作用:与select一样,但是select对文件描述符数量有最大限制,一般为1024,poll没有。
函数原型
在这里插入图片描述

示例代码

void main(void)
{
	int ret;
	int fd; /* 要监视的文件描述符 */
	struct pollfd fds; 
	
	fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
	
	/* 构造结构体 */
	fds.fd = fd;
	fds.events = POLLIN; /* 监视数据是否可以读取 */
	
	ret = poll(&fds, 1, 500); /* 轮询文件是否可操作,超时 500ms */
	if (ret) { /* 数据有效 */
	......
	/* 读取数据 */
	......
	} else if (ret == 0) { /* 超时 */
	......
	} else if (ret < 0) { /* 错误 */
	......
	}
}
epoll

作用:传统的 selcet 和 poll 函数都会随着所监听的 fd 数量的增加,出现效率低下的问题,而且poll 函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。为此,epoll 就是为处理大并发而准备的,一般常常在网络编程中使用 epoll 函数。
函数原型及相关API函数
在这里插入图片描述
在这里插入图片描述

Linux驱动下的poll操作函数

定义:当应用程序调用 select 或 poll 函数来对驱动程序进行非阻塞访问的时候,驱动程序file_operations操作集中的poll函数就会执行,就跟read,write函数类似。所以驱动程序的编写者需要提供对应的poll函数。

函数原型
在这里插入图片描述
在这里插入图片描述

异步通知

定位:相当于中断的作用,只不过中断是硬件层面的,异步通知采用的是信号,是软件层面进行模拟的中断信号

信号类型

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

驱动程序中的信号处理

结构体定义:一般将其放至设备结构体中
在这里插入图片描述

struct dev{
	dev_t devid; 
	struct cdev cdev; 
	struct class *class;
	......
	struct fasync_struct *async_queue; /* fasync_struct 结构体 */
};

fasync 函数
在这里插入图片描述

kill_fasync 函数
在这里插入图片描述

应用程序中的信号处理

过程分三步

1. 注册信号处理函数

sighandler_t signal(int signum, sighandler_t handler)

2. 将本应用程序的进程号告诉给内核

fcntl(fd, F_SETOWN, getpid())
3. 开启异步通知
flags = fcntl(fd, F_GETFL); 		/* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC); /* 开启当前进程异步通知功能 */
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

煜个头头

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值