在正常驱动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); /* 开启当前进程异步通知功能 */