drivers_day10

本文详细介绍了Linux内核如何解决竞态问题,包括并发、竞态、互斥访问、临界区及共享资源的概念,以及中断、进程抢占等情况引发的竞态。重点阐述了中断屏蔽、原子操作、自旋锁、衍生自旋锁、信号量等解决方法。此外,文章深入分析了Linux内核等待队列机制在应用程序串口工具操作硬件设备过程中的应用,包括如何让进程在内核空间进行休眠及休眠状态的管理。

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

回顾:

linux内核如何解决竞态:

1.概念

并发:多个执行单元同时发生!

竞态:并发的多个执行单元同时访问共享资源引起的竞争状态!

互斥访问:当有多个执行单元对共享资源进行访问时,只允许一个执行单元对共享资源进行访问,其他执行单元禁止访问共享资源!

临界区:访问共享资源的代码区,所以互斥访问就是对临界区的互斥访问!

共享资源

休眠

2.形成竞态的情况

CPU多个cpu之间共享系统总线、共享内存、外存、系统IO导致竞态!

进程与进程的抢占:linux支持进程之间的抢占!

中断和进程:中断的优先级高于进程的优先级

中断和中断:硬件中断的优先级高于软件的优先级,软件又分为两个优先级(高、低)!

3.linux内核提供的解决竞态的方法:

中断屏蔽:在对临界区的访问之前进行中断的屏蔽,在临界区的访问之后进行中断的使能!

local_irq_disable();/local_irq_enable();

unsigned long flags;

local_irq_save(flags);/local_irq_restore(flags);

解决进程之间的抢占、进程与中断、中断之间的竞态问题!

原子操作:

       位原子操作

       整型原子操作

       ldrex,strex:多核

自旋锁

衍生自旋锁

信号量

**********************************************************

linux内核等待队列机制:

案例:分析应用程序串口工具操作串口硬件设备的过程。

1.外设的处理速度要远远慢于CPU!

2.应用程序在用户空间没有权利访问硬件设备,只有通过系统调用跑到内核空间才有权限访问硬件设备!

3.一个应用程序读取串口硬件设备采用两种方法:

轮询方式:相当的耗费CPU的资源,让CPU做大量的无用功!只能做一件事,就是不停的查看是否有数据的到来!

中断方式:CPU一旦发现串口设备不可读(没数据),CPU干别的事情,一旦串口接收到数据,给CPU产生一个接收中断信号,让CPU来获取串口数据。问:这个应用程序在做什么呢?

当串口设备没有接收到数据,应用程序一旦发现,利用内核提供的睡眠机制,应用在内核空间进入休眠状态;一旦串口设备给CPU产生中断信号,中断信号的到来也就代表这数据的到来,这时只需唤醒休眠的应用程序,让应用程序读取串口数据。

 

:如果设备数据没有准备就绪,如何让进程在内核空间进行休眠

答:linux内核等待队列机制

本质目的:就是让进程在内核空间进行休眠

注意:区别于工作队列

工作队列:是中断底半部的机制,是实现延后执行的一种手段

等待队列:是让进程在内核空间进行休眠的

但是它们针对处理的对象都是进程!

 

进程的状态:

运行:TASK_RUNNING

准备就绪:TASK_READY

可中断休眠:TASK_INTERRUPTIBLE

不可中断休眠:TASK_UNINTERRUPTIBLE

进程要“休眠":要休眠进程会将CPU资源全部从当前进程中撤下来,将CPU资源给别的任务去使用,比如另外一个进程;

进程之间的切换:由内核调度器来实现,这个调度器就是用来管理进程的!

 

linux内核等待队列机制实现过程:

老鹰-》调度器:内核已经实现

鸡妈妈-》等待队列头

小鸡-》休眠的进程

 

内核描述等待队列头涉及的数据类型:

wait_queue_head_t

内核描述休眠的进程,装载休眠进程的容器的数据类型:

wait_queue_t

 

等待队列让进程休眠的方法,而不是唤醒:

方法1:

步骤:

1.分配等待队列头

  wait_queue_head_t wq;

2.初始化等待队列头

  init_waitqueue_head(&wq);

3.如果一个进程要访问设备,发现设备不可用,进入休眠,此时分配这个进程的休眠的容器

  DECLARE_WAITQUEUE(wait, current);

  wait:表示保存当前进程的容器

  current:它是内核的全局变量,在linux内核中,内核用struct  task_struct结构体来描述每一个进程,那么当进程获取CPU资源时,current就指向当前进程(哪个进程获取CPU资源,current就指向这个进程对应的task_struct结构体对象)

  例如打印出当前进程的pid和name

 printk("current process name is %s pid is %d\n",

              current->comm, current->pid);

  或者:

  wait_queue_t wait;

 init_waitqueue_entry(&wait, current);

注意:如果有多个休眠的进程,必须为每一个进程分配一个容器,并且current也会分别指向不同的进程!

4.然后将当前进程添加到休眠的队列中去

  add_wait_queue(&wq, &wait);

   注意:仅仅是将当前进程添加到这个队列中去,但进程还处于运行状态!

 

5.设置当前进程的休眠状态(可中断或者不可中断)

可中断的休眠状态:

current->state =TASK_INTERRUPTIBLE;

不可中断的休眠状态:

current->state =TASK_UNINTERRUPTIBLE;

 

6.让进程进入真正的休眠状态

   schedule(); //启动调度器,并且让CPU资源从当前进程撤下来,给别的进程去使用,至此当前进程运行到这个地方就停止不动!一旦被唤醒,这个函数返回,当前进程接着执行

 

7.如果进程被设置为可中断的休眠状态,进程被唤醒的方法有两种:

硬件设备可用,产生中断,由中断来唤醒;

进程接收到了信号引起的唤醒,所以要判断唤醒的原因:

判断是否接收到信号引起的唤醒:

if (signal_pending(current)) {

  printk("当前进程是由于接收到了信号引起的唤醒");

  printk("给用户返回-ERESTARTSYS");

} else {

  printk("中断引起的唤醒,说明数据可用");

  printk("后续继续获取数据");

}

 

8.不管是硬件中断引起的唤醒还是信号引起的唤醒,重新设置当前进程的状态为运行状态

current->state = TASK_RUNNING;

 

9.将当前进程从休眠队列中移除

  remove_wait_queue&wq, &wait);

 

参考代码:有一个进程读取按键数据:

wait_queue_head_t wq; //全局变量

驱动入口函数或者open函数:

init_waitqueue_head(&wq);

驱动read函数:

static ssize_t btn_read(...)

{

 wait_queue_t wait; //分配一个当前进程的容器

 init_waitqueue_entry(&wait, current);       //把当前进程添加到这个容器中

add_wait_queue(&wq, &wait);//将当前进程添加到休眠队列中

current->state = TASK_INTERRUPTIBLE;//设置当前进程的休眠状态

schedule(); //进入真正的休眠,一旦被唤醒,进程接着执行

//判断唤醒的原因

if (signal_pending(current)) {

  printk("接收到了信号引起的唤醒");

  ret = -ERESTARTSYS;

} else {

  printk("按键有操作,产生中断引起的唤醒");

}

current->state = TASK_RUNNING;//设置当前进程的状态为运行

remove_wait_queue(&wq, &wait);//从休眠队列中移出

上报按键数据

return ret;

}

 

唤醒的方法有两种:

1.接收到信号引起的唤醒

2.驱动主动唤醒休眠的进程,方法如下:

wake_up(wait_queue_head_t *queue);

       唤醒由queue指向的等待队列数据链中的所有睡眠类型的等待进程

wake_up_interruptible(wait_queue_head_t *queue);

       唤醒由queue指向的等待队列数据链中的所有睡眠类型为TASK_INTERRUPTIBLE的等待进程

 

案例1:实现读进程唤醒写进程,写进程唤醒读进程

实验步骤:

insmod btn_drv.ko

./led_test r & //读进程

./led_test w & //写进程

./led_test r & //读进程

kill 读进程或者写进程

 

案例2:根据以上案例,在底层驱动的read函数能够给用户上报一个按键的信息(键值和按键的状态),提示可以把底层驱动的write函数作为中断来使用。

 

案例3:利用等待队列,实现按键驱动,要求按键上报的信息为键值和按键的状态!例如:

KEY_UP: 0x50

KEY_DOWN:0x51

按键状态:按下为1,松开为0

分析:

read->fops->cdev->中断->休眠->等待队列

 

指定超时时间的休眠:

schedule()换成schedule_timeout(5*HZ);

前者的休眠是永久休眠(没有被驱动主动唤醒或者接收到信号)

后者的休眠是指定了睡眠的时间,例如5秒,如果没有接收到信号,也没有接收到驱动主动唤醒,一旦5秒到期,此函数也会返回,返回0,否则返回非0(驱动主动唤醒或者接收到了信号)!

案例:实现按键驱动,指定超时,不是永久休眠!

 

方法2:

1.分配等待队列头

  wait_queue_head_t wq;

2.初始化等待队列头

   init_waitqueue_head(&wq);

3.如果进程进入休眠状态:

  wait_event(wq, condition);

  condition为真,立即返回,不休眠

  condition为假,进程进入不可中断的休眠状态,进程被添加到wq所对应的等待队列头中

   或者

  wait_event_interruptible(wq, condition);

  condition为真,立即返回,不休眠

  condition为假,进程进入可中断的休眠状态,进程被添加      到wq所对应的等待队列头中

   或者

  wait_event_timeout(wq, condition, 5*HZ);

  condition为真,立即返回,不休眠

  condition为假,进程进入不可中断的休眠状态,进程被添加   到wq所对应的等待队列头中,并且超时时间到期,进程也被唤醒;

   或者

  wait_event_interruptible_timeout(wq,                  condition,5*HZ);

   condition为真,立即返回,不休眠

  condition为假,进程进入可中断的休眠状态,进程被添加   到wq所对应的等待队列头中,并且超时时间到期,进程也被唤醒;

 

总结:以上宏涉及的condition其实就是代表是否是驱动主动唤醒,如果驱动主动唤醒,应该让condition设置为真,否则还是为假!

 

案例:利用等待队列编程方法2来实现按键驱动

 

 

 

 

 

 

 

 

 

 

 

 

 

 

#include <avr/io.h> #include <avr/interrupt.h> #include <util/delay.h> #include <avr/eeprom.h> #include <string.h> #define delay_ms(x) _delay_ms(x) // LCD 相关引脚定义 #define LCD_RS PC0 #define LCD_RW PC1 #define LCD_E PC2 #define LCD_D4 PA4 #define LCD_D5 PA5 #define LCD_D6 PA6 #define LCD_D7 PA7 // 按键引脚定义(使用PORTD) #define SET_BUTTON PIND0 #define ADD_BUTTON PIND1 #define SUB_BUTTON PIND2 // LED 引脚定义 #define LED_PIN PB3 // EEPROM存储地址 #define EEPROM_TIME_HOUR_ADDR 0x00 #define EEPROM_TIME_MINUTE_ADDR 0x01 #define EEPROM_DATE_YEAR_ADDR 0x02 #define EEPROM_DATE_MONTH_ADDR 0x03 #define EEPROM_DATE_DAY_ADDR 0x04 // 全局变量 volatile uint8_t hour = 12; // 小时(0-23) volatile uint8_t minute = 0; // 分钟(0-59) volatile uint8_t second = 0; // 秒(0-59) volatile uint8_t year = 23; // 年份(后两位) volatile uint8_t month = 1; // 月份(1-12) volatile uint8_t day = 1; // 日期(1-31) volatile uint8_t set_mode = 0; // 0:正常显示, 1:设置小时, 2:设置分钟, 3:设置年, 4:设置月, 5:设置日 volatile uint8_t blink_state = 0; // 闪烁状态 volatile uint8_t timer1_counter = 0; // 定时器计数器 // 闹钟结构 typedef struct { uint8_t hour; uint8_t minute; uint8_t enabled; } Alarm; Alarm alarm1 = {8, 0, 0}; // 闹钟1,默认8:00,关闭 Alarm alarm2 = {12, 0, 0}; // 闹钟2,默认12:00,关闭 // LCD初始化 void LCD_Init() { // 设置数据端口为输出 DDRA |= (1<<LCD_D4) | (1<<LCD_D5) | (1<<LCD_D6) | (1<<LCD_D7); // 设置控制端口为输出 DDRC |= (1<<LCD_RS) | (1<<LCD_RW) | (1<<LCD_E); // 初始化序列 delay_ms(50); // 等待LCD上电稳定 // 4位模式初始化 LCD_Write_Command(0x33); LCD_Write_Command(0x32); LCD_Write_Command(0x28); // 4位模式,2行显示,5x8点阵 LCD_Write_Command(0x0C); // 显示开,光标关,闪烁关 LCD_Write_Command(0x06); // 增量模式,不移位 LCD_Write_Command(0x01); // 清屏 delay_ms(2); } // 向LCD发送命令 void LCD_Write_Command(unsigned char cmd) { PORTC &= ~(1<<LCD_RS); // RS=0 命令模式 PORTC &= ~(1<<LCD_RW); // RW=0 写入 // 发送高4位 PORTA = (PORTA & 0x0F) | (cmd & 0xF0); PORTC |= (1<<LCD_E); // E=1 delay_ms(1); PORTC &= ~(1<<LCD_E); // E=0 // 发送低4位 PORTA = (PORTA & 0x0F) | ((cmd << 4) & 0xF0); PORTC |= (1<<LCD_E); // E=1 delay_ms(1); PORTC &= ~(1<<LCD_E); // E=0 delay_ms(1); } // 向LCD发送数据 void LCD_Write_Data(unsigned char data) { PORTC |= (1<<LCD_RS); // RS=1 数据模式 PORTC &= ~(1<<LCD_RW); // RW=0 写入 // 发送高4位 PORTA = (PORTA & 0x0F) | (data & 0xF0); PORTC |= (1<<LCD_E); // E=1 delay_ms(1); PORTC &= ~(1<<LCD_E); // E=0 // 发送低4位 PORTA = (PORTA & 0x0F) | ((data << 4) & 0xF0); PORTC |= (1<<LCD_E); // E=1 delay_ms(1); PORTC &= ~(1<<LCD_E); // E=0 delay_ms(1); } // 在LCD上显示字符串 void LCD_Display_String(char *str) { while (*str) { LCD_Write_Data(*str++); } } // 设置LCD光标位置 void LCD_Set_Cursor(uint8_t row, uint8_t col) { uint8_t address; if (row == 0) { address = 0x80 + col; } else { address = 0xC0 + col; } LCD_Write_Command(address); } // 显示时间 void Display_Time() { char buffer[16]; // 第一行显示时间 LCD_Set_Cursor(0, 4); sprintf(buffer, "%02d:%02d:%02d", hour, minute, second); LCD_Display_String(buffer); // 第二行显示日期 LCD_Set_Cursor(1, 4); sprintf(buffer, "20%02d-%02d-%02d", year, month, day); LCD_Display_String(buffer); } // 显示设置模式 void Display_Set_Mode() { char buffer[16]; LCD_Set_Cursor(0, 0); LCD_Display_String("Set:"); switch(set_mode) { case 1: // 设置小时 if(blink_state) { sprintf(buffer, "Hour: %02d", hour); } else { sprintf(buffer, "Hour: "); } break; case 2: // 设置分钟 if(blink_state) { sprintf(buffer, "Minute:%02d", minute); } else { sprintf(buffer, "Minute: "); } break; case 3: // 设置年 if(blink_state) { sprintf(buffer, "Year: 20%02d", year); } else { sprintf(buffer, "Year: "); } break; case 4: // 设置月 if(blink_state) { sprintf(buffer, "Month: %02d", month); } else { sprintf(buffer, "Month: "); } break; case 5: // 设置日 if(blink_state) { sprintf(buffer, "Day: %02d", day); } else { sprintf(buffer, "Day: "); } break; default: sprintf(buffer, "Normal Mode "); } LCD_Set_Cursor(0, 5); LCD_Display_String(buffer); // 仍然显示时间日期 LCD_Set_Cursor(1, 0); sprintf(buffer, "Time:%02d:%02d:%02d", hour, minute, second); LCD_Display_String(buffer); } // 从EEPROM加载时间日期 void Load_Time_From_EEPROM() { hour = eeprom_read_byte((uint8_t*)EEPROM_TIME_HOUR_ADDR); minute = eeprom_read_byte((uint8_t*)EEPROM_TIME_MINUTE_ADDR); year = eeprom_read_byte((uint8_t*)EEPROM_DATE_YEAR_ADDR); month = eeprom_read_byte((uint8_t*)EEPROM_DATE_MONTH_ADDR); day = eeprom_read_byte((uint8_t*)EEPROM_DATE_DAY_ADDR); // 检查读取的值是否有效 if(hour > 23) hour = 0; if(minute > 59) minute = 0; if(year > 99) year = 23; if(month == 0 || month > 12) month = 1; if(day == 0 || day > 31) day = 1; } // 保存时间日期到EEPROM void Save_Time_To_EEPROM() { eeprom_update_byte((uint8_t*)EEPROM_TIME_HOUR_ADDR, hour); eeprom_update_byte((uint8_t*)EEPROM_TIME_MINUTE_ADDR, minute); eeprom_update_byte((uint8_t*)EEPROM_DATE_YEAR_ADDR, year); eeprom_update_byte((uint8_t*)EEPROM_DATE_MONTH_ADDR, month); eeprom_update_byte((uint8_t*)EEPROM_DATE_DAY_ADDR, day); } // 初始化定时器1 (1秒中断) void Timer1_Init() { // 设置定时器1为CTC模式 TCCR1B |= (1 << WGM12); // 设置预分频为1024 TCCR1B |= (1 << CS12) | (1 << CS10); // 设置比较值 (16MHz/1024 = 15625 ticks/sec, 15625 ticks = 1秒) OCR1A = 15625; // 启用比较匹配中断 TIMSK |= (1 << OCIE1A); } // 初始化按键引脚 void Buttons_Init() { // 设置按键引脚为输入,启用上拉电阻 DDRD &= ~((1<<SET_BUTTON) | (1<<ADD_BUTTON) | (1<<SUB_BUTTON)); PORTD |= (1<<SET_BUTTON) | (1<<ADD_BUTTON) | (1<<SUB_BUTTON); } // 按键扫描 void Key_Scan() { static uint8_t last_set_state = 1; static uint8_t last_add_state = 1; static uint8_t last_sub_state = 1; uint8_t current_set_state = PIND & (1<<SET_BUTTON); uint8_t current_add_state = PIND & (1<<ADD_BUTTON); uint8_t current_sub_state = PIND & (1<<SUB_BUTTON); // 检测设置按键按下 if(last_set_state && !current_set_state) { _delay_ms(20); // 消抖 if(!(PIND & (1<<SET_BUTTON))) { set_mode++; if(set_mode > 5) set_mode = 0; if(set_mode == 0) { // 退出设置模式,保存时间到EEPROM Save_Time_To_EEPROM(); } } } // 只在设置模式下检测加减按键 if(set_mode > 0) { // 检测增加按键按下 if(last_add_state && !current_add_state) { _delay_ms(20); // 消抖 if(!(PIND & (1<<ADD_BUTTON))) { switch(set_mode) { case 1: // 增加小时 hour = (hour + 1) % 24; break; case 2: // 增加分钟 minute = (minute + 1) % 60; break; case 3: // 增加年 year = (year + 1) % 100; break; case 4: // 增加月 month = (month % 12) + 1; break; case 5: // 增加日 day = (day % 31) + 1; break; } } } // 检测减少按键按下 if(last_sub_state && !current_sub_state) { _delay_ms(20); // 消抖 if(!(PIND & (1<<SUB_BUTTON))) { switch(set_mode) { case 1: // 减少小时 hour = (hour == 0) ? 23 : hour - 1; break; case 2: // 减少分钟 minute = (minute == 0) ? 59 : minute - 1; break; case 3: // 减少年 year = (year == 0) ? 99 : year - 1; break; case 4: // 减少月 month = (month == 1) ? 12 : month - 1; break; case 5: // 减少日 day = (day == 1) ? 31 : day - 1; break; } } } } last_set_state = current_set_state; last_add_state = current_add_state; last_sub_state = current_sub_state; } // 检查闹钟 void Check_Alarms() { static uint8_t alarm1_triggered = 0; static uint8_t alarm2_triggered = 0; // 检查闹钟1 if(alarm1.enabled && !alarm1_triggered && hour == alarm1.hour && minute == alarm1.minute && second == 0) { alarm1_triggered = 1; PORTB |= (1<<LED_PIN); // 打开LED } // 检查闹钟2 if(alarm2.enabled && !alarm2_triggered && hour == alarm2.hour && minute == alarm2.minute && second == 0) { alarm2_triggered = 1; PORTB |= (1<<LED_PIN); // 打开LED } // 每分钟重置闹钟触发标志 if(second == 0) { alarm1_triggered = 0; alarm2_triggered = 0; } // 如果LED亮起,检测按键关闭 if(PORTB & (1<<LED_PIN)) { if(!(PIND & (1<<SET_BUTTON)) || !(PIND & (1<<ADD_BUTTON)) || !(PIND & (1<<SUB_BUTTON))) { PORTB &= ~(1<<LED_PIN); // 关闭LED } } } // 定时器1比较匹配中断服务程序 ISR(TIMER1_COMPA_vect) { // 更新时间 second++; if(second >= 60) { second = 0; minute++; if(minute >= 60) { minute = 0; hour++; if(hour >= 24) { hour = 0; // 日期增加逻辑 day++; uint8_t max_day = 31; // 处理不同月份的天数 if(month == 4 || month == 6 || month == 9 || month == 11) { max_day = 30; } else if(month == 2) { // 简单处理2月天数(不考虑闰年) max_day = 28; } if(day > max_day) { day = 1; month++; if(month > 12) { month = 1; year++; if(year > 99) year = 0; } } } } } // 更新闪烁状态 (0.5秒周期) timer1_counter++; if(timer1_counter >= 5) { // 10次 = 1秒 (0.1秒中断) timer1_counter = 0; blink_state = !blink_state; } // 检查闹钟 Check_Alarms(); } int main(void) { // 初始化端口 DDRB |= (1<<LED_PIN); // LED引脚为输出 PORTB &= ~(1<<LED_PIN); // 初始关闭LED // 初始化按键 Buttons_Init(); // 初始化LCD LCD_Init(); // 从EEPROM加载时间 Load_Time_From_EEPROM(); // 初始化定时器 Timer1_Init(); // 启用全局中断 sei(); // 清屏并显示欢迎信息 LCD_Write_Command(0x01); LCD_Set_Cursor(0, 3); LCD_Display_String("AVR Clock"); LCD_Set_Cursor(1, 2); LCD_Display_String("Initializing..."); delay_ms(1000); LCD_Write_Command(0x01); while(1) { // 扫描按键 Key_Scan(); // 更新显示 if(set_mode == 0) { Display_Time(); } else { Display_Set_Mode(); } // 短暂延迟 delay_ms(100); } return 0; }使用的atmega128单片机,不改动引脚的使用
05-27
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值