Linux中断(interrupt)子系统之三:中断流控处理层

本文深入解析Linux内核中断流控层的设计与实现,包括不同类型的中断处理机制,如电平触发、边沿触发等,以及如何正确配置中断以避免重复响应等问题。

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

1.  中断流控层简介

早期的内核版本中,几乎所有的中断都是由__do_IRQ函数进行处理,但是,因为各种中断请求的电气特性会有所不同,又或者中断控制器的特性也不同,这会导致以下这些处理也会有所不同:

  • 何时对中断控制器发出ack回应;
  • mask_irq和unmask_irq的处理;
  • 中断控制器是否需要eoi回应?
  • 何时打开cpu的本地irq中断?以便允许irq的嵌套;
  • 中断数据结构的同步和保护;
/*****************************************************************************************************/
声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!
/*****************************************************************************************************/
为此,通用中断子系统把几种常用的流控类型进行了抽象,并为它们实现了相应的标准函数,我们只要选择相应的函数,赋值给irq所对应的irq_desc结构的handle_irq字段中即可。这些标准的回调函数都是irq_flow_handler_t类型:

typedef	void (*irq_flow_handler_t)(unsigned int irq,
					    struct irq_desc *desc);
目前的通用中断子系统实现了以下这些标准流控回调函数,这些函数都定义在:kernel/irq/chip.c中,

  • handle_simple_irq  用于简易流控处理;
  • handle_level_irq  用于电平触发中断的流控处理;
  • handle_edge_irq  用于边沿触发中断的流控处理;
  •  handle_fasteoi_irq  用于需要响应eoi的中断控制器;
  • handle_percpu_irq  用于只在单一cpu响应的中断;
  • handle_nested_irq  用于处理使用线程的嵌套中断;

驱动程序和板级代码可以通过以下几个API设置irq的流控函数:

  • irq_set_handler();
  • irq_set_chip_and_handler();
  • irq_set_chip_and_handler_name();

以下这个序列图展示了整个通用中断子系统的中断响应过程,flow_handle一栏就是中断流控层的生命周期:


                                                                                           图1.1  通用中断子系统的中断响应过程

2.  handle_simple_irq

该函数没有实现任何实质性的流控操作,在把irq_desc结构锁住后,直接调用handle_irq_event处理irq_desc中的action链表,它通常用于多路复用(类似于中断控制器级联)中的子中断,由父中断的流控回调中调用。或者用于无需进行硬件控制的中断中。以下是它的经过简化的代码:

void
handle_simple_irq(unsigned int irq, struct irq_desc *desc)
{
	raw_spin_lock(&desc->lock);
	......
	handle_irq_event(desc);

out_unlock:
	raw_spin_unlock(&desc->lock);
}

3.  handle_level_irq

该函数用于处理电平中断的流控操作。电平中断的特点是,只要设备的中断请求引脚(中断线)保持在预设的触发电平,中断就会一直被请求,所以,为了避免同一中断被重复响应,必须在处理中断前先把mask irq,然后ack irq,以便复位设备的中断请求引脚,响应完成后再unmask irq。实际的情况稍稍复杂一点,在mask和ack之后,还要判断IRQ_INPROGRESS标志位,如果该标志已经置位,则直接退出,不再做实质性的处理,IRQ_INPROGRESS标志在handle_irq_event的开始设置,在handle_irq_event结束时清除,如果监测到IRQ_INPROGRESS被置位,表明该irq正在被另一个CPU处理中,所以直接退出,对电平中断来说是正确的处理方法。但是我觉得在ARM系统中,这种情况根本就不会发生,因为在没有进入handle_level_irq之前,中断控制器没有收到ack通知,它不会向第二个CPU再次发出中断请求,而当程序进入handle_level_irq之后,第一个动作就是mask irq,然后ack irq(通常是联合起来的:mask_ack_irq),这时候就算设备再次发出中断请求,也是在handle_irq_event结束,unmask irq之后,这时IRQ_INPROGRESS标志已经被清除。我不知道其他像X86之类的体系是否有不同的行为,有知道的朋友请告知我一下。以下是handle_level_irq经过简化之后的代码:
void
handle_level_irq(unsigned int irq, struct irq_desc *desc)
{
	raw_spin_lock(&desc->lock);
	mask_ack_irq(desc);

	if (unlikely(irqd_irq_inprogress(&desc->irq_data)))
			goto out_unlock;
        ......

	if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data)))
		goto out_unlock;

	handle_irq_event(desc);

	if (!irqd_irq_disabled(&desc->irq_data) && !(desc->istate & IRQS_ONESHOT))
		unmask_irq(desc);
out_unlock:
	raw_spin_unlock(&desc->lock);
}
虽然handle_level_irq对电平中断的流控进行了必要的处理,因为电平中断的特性:只要没有ack irq,中断线会一直有效,所以我们不会错过某次中断请求,但是驱动程序的开发人员如果对该过程理解不透彻,特别容易发生某次中断被多次处理的情况。特别是使用了中断线程(action->thread_fn)来响应中断的时候:通常mask_ack_irq只会清除中断控制器的pending状态,很多慢速设备(例如通过i2c或spi控制的设备)需要在中断线程中清除中断线的pending状态,但是未等到中断线程被调度执行的时候,handle_level_irq早就返回了,这时已经执行过unmask_irq,设备的中断线pending处于有效状态,中断控制器会再次发出中断请求,结果是设备的一次中断请求,产生了两次中断响应。要避免这种情况,最好的办法就是不要单独使用中断线程处理中断,而是要实现request_threaded_irq()的第二个参数irq_handler_t:handler,在handle回调中使用disable_irq()关闭该irq,然后在退出中断线程回调前再enable_irq()。假设action->handler没有屏蔽irq,以下这幅图展示了电平中断期间IRQ_PROGRESS标志、本地中断状态和触发其他CPU的状态:
                                          图3.1  电平触发中断状态
上图中颜色分别代表不同的状态:
状态红色绿色
IRQ_PROGRESS           TRUE       FALSE
是否允许本地cpu中断            禁止                允许  
是否允许该设备再次触发中断(可能由其它cpu响应)            禁止          允许

4.  handle_edge_irq

该函数用于处理边沿触发中断的流控操作。边沿触发中断的特点是,只有设备的中断请求引脚(中断线)的电平发生跳变时(由高变低或者有低变高),才会发出中断请求,因为跳变是一瞬间,而且不会像电平中断能保持住电平,所以处理不当就特别容易漏掉一次中断请求,为了避免这种情况,屏蔽中断的时间必须越短越好。内核的开发者们显然意识到这一点,在正是处理中断前,判断IRQ_PROGRESS标志没有被设置的情况下,只是ack irq,并没有mask irq,以便复位设备的中断请求引脚,在这之后的中断处理期间,另外的cpu可以再次响应同一个irq请求,如果IRQ_PROGRESS已经置位,表明另一个CPU正在处理该irq的上一次请求,这种情况下,他只是简单地设置IRQS_PENDING标志,然后mask_ack_irq后退出,中断请求交由原来的CPU继续处理。因为是mask_ack_irq,所以系统实际上只允许挂起一次中断。
	if (unlikely(irqd_irq_disabled(&desc->irq_data) ||
		     irqd_irq_inprogress(&desc->irq_data) || !desc->action)) {
		if (!irq_check_poll(desc)) {
			desc->istate |= IRQS_PENDING;
			mask_ack_irq(desc);
			goto out_unlock;
		}
	}

	desc->irq_data.chip->irq_ack(&desc->irq_data);
从上面的分析可以知道,处理中断期间,另一次请求可能由另一个cpu响应后挂起,所以在处理完本次请求后还要判断IRQS_PENDING标志,如果被置位,当前cpu要接着处理被另一个cpu“委托”的请求。内核在这里设置了一个循环来处理这种情况,直到IRQS_PENDING标志无效为止,而且因为另一个cpu在响应并挂起irq时,会mask irq,所以在循环中要再次unmask irq,以便另一个cpu可以再次响应并挂起irq:
	do {
                ......
		if (unlikely(desc->istate & IRQS_PENDING)) {
			if (!irqd_irq_disabled(&desc->irq_data) &&
			    irqd_irq_masked(&desc->irq_data))
				unmask_irq(desc);
		}

		handle_irq_event(desc);

	} while ((desc->istate & IRQS_PENDING) &&
		 !irqd_irq_disabled(&desc->irq_data));
IRQS_PENDING标志会在handle_irq_event中清除。

                                 图4.1   边沿触发中断状态
上图中颜色分别代表不同的状态:
状态        红色        绿色
IRQ_PROGRESS        TRUE        FALSE
是否允许本地cpu中断        禁止        允许
是否允许该设备再次触发中断(可能由其它cpu响应)        禁止        允许
是否处于中断上下文    处于中断上下文    处于进程上下文

由图4.1也可以看出,在处理软件中断(softirq)期间,此时仍然处于中断上下文中,但是cpu的本地中断是处于打开状态的,这表明此时嵌套中断允许发生,不过这不要紧,因为重要的处理已经完成,被嵌套的也只是软件中断部分而已。这个也就是内核区分top和bottom两个部分的初衷吧。

5.  handle_fasteoi_irq

现代的中断控制器通常会在硬件上实现了中断流控功能,例如ARM体系中的GIC通用中断控制器。对于这种中断控制器,CPU只需要在每次处理完中断后发出一个end of interrupt(eoi),我们无需关注何时mask,何时unmask。不过虽然想着很完美,事情总有特殊的时候,所以内核还是给了我们插手的机会,它利用irq_desc结构中的preflow_handler字段,在正式处理中断前会通过preflow_handler函数调用该回调。
void
handle_fasteoi_irq(unsigned int irq, struct irq_desc *desc)
{
	raw_spin_lock(&desc->lock);

	if (unlikely(irqd_irq_inprogress(&desc->irq_data)))
		if (!irq_check_poll(desc))
			goto out;
        ......
	if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
		desc->istate |= IRQS_PENDING;
		mask_irq(desc);
		goto out;
	}

	if (desc->istate & IRQS_ONESHOT)
		mask_irq(desc);

	preflow_handler(desc);
	handle_irq_event(desc);

out_eoi:
	desc->irq_data.chip->irq_eoi(&desc->irq_data);
out_unlock:
	raw_spin_unlock(&desc->lock);
	return;
        ......
}
此外,内核还提供了另外一个eoi版的函数: handle_edge_eoi_irq,它的处理类似于handle_edge_irq,只是无需实现mask和unmask的逻辑。

6.  handle_percpu_irq

该函数用于smp系统,当某个irq只在一个cpu上处理时,我们可以无需用自旋锁对数据进行保护,也无需处理cpu之间的中断嵌套重入,所以函数很简单:
void
handle_percpu_irq(unsigned int irq, struct irq_desc *desc)
{
	struct irq_chip *chip = irq_desc_get_chip(desc);

	kstat_incr_irqs_this_cpu(irq, desc);

	if (chip->irq_ack)
		chip->irq_ack(&desc->irq_data);

	handle_irq_event_percpu(desc, desc->action);

	if (chip->irq_eoi)
		chip->irq_eoi(&desc->irq_data);
}

7.  handle_nested_irq

该函数用于实现其中一种中断共享机制,当多个中断共享某一根中断线时,我们可以把这个中断线作为父中断,共享该中断的各个设备作为子中断,在父中断的中断线程中决定和分发响应哪个设备的请求,在得出真正发出请求的子设备后,调用handle_nested_irq来响应中断。所以,该函数是在进程上下文执行的,我们也无需扫描和执行irq_desc结构中的action链表。父中断在初始化时必须通过irq_set_nested_thread函数明确告知中断子系统:这些子中断属于线程嵌套中断类型,这样驱动程序在申请这些子中断时,内核不会为它们建立自己的中断线程,所有的子中断共享父中断的中断线程。

void handle_nested_irq(unsigned int irq)
{
	......
        might_sleep();

	raw_spin_lock_irq(&desc->lock);
        ......
	action = desc->action;
	if (unlikely(!action || irqd_irq_disabled(&desc->irq_data)))
		goto out_unlock;

	irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);
	raw_spin_unlock_irq(&desc->lock);

	action_ret = action->thread_fn(action->irq, action->dev_id);

	raw_spin_lock_irq(&desc->lock);
	irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);

out_unlock:
	raw_spin_unlock_irq(&desc->lock);
}

<think>好的,用户之前询问了UART_startup的调用流程和应用场景,现在补充了条件是在Linux操作系统下。我需要调整之前的回答,把重点从裸机或RTOS环境转移到Linux的驱动和用户空间应用。 首先,用户可能是在进行嵌入式Linux开发,需要了解如何在Linux下初始化和使用UART。之前的回答基于HAL库,比如STM32的,但Linux环境下更多是通过内核驱动和sysfs或设备文件来操作。 用户的需求可能包括了解内核驱动中的UART初始化流程,以及用户空间如何配置和使用串口。需要解释设备树(DTS)的作用,比如如何配置UART引脚、波特率等参数。同时,用户可能想知道如何通过系统调用或工具如stty进行配置,以及相关的系统守护进程如getty如何管理串口登录。 另外,用户可能对Linux下的调试工具感兴趣,比如使用echo/cat操作设备文件,或者使用screen、minicom等工具。需要注意权限问题,比如用户需要属于dialout组才能访问串口设备。 还要考虑动态调整的场景,比如修改波特率后是否需要重新初始化,或者通过ioctl进行实时配置。错误处理方面,可能需要查看内核日志dmesg,或者处理打开设备时的错误码。 最后,要确保回答结构清晰,分内核空间和用户空间,调用流程和应用场景分开,并补充注意事项,如权限、内核版本差异等。同时,保持使用正确的LaTeX格式和代码块,符合用户之前的格式要求。</think>在Linux操作系统下,UART设备的初始化和管理流程与裸机开发有显著差异。以下是针对Linux环境的补充说明: --- ### **一、Linux内核空间UART初始化流程** 1. **设备树配置(DTS)** ```dts &uart1 { pinctrl-names = "default"; pinctrl-0 = <&uart1_pins>; status = "okay"; clock-frequency = <16000000>; current-speed = <115200>; }; ``` - 通过设备树定义UART引脚复用、时钟频率、默认波特率等 - 内核启动时自动解析并注册设备 2. **驱动加载阶段** ```c // 驱动入口函数 static int __init uart_driver_init(void) { platform_driver_register(&uart_platform_driver); tty_register_driver(&uart_tty_driver); return 0; } ``` - 注册平台驱动`platform_driver` - 关联TTY设备驱动`tty_driver` 3. **硬件初始化** ```c static int uart_startup(struct uart_port *port) { // 1. 时钟使能 clk_prepare_enable(port->clk); // 2. GPIO复用配置 pinctrl_pm_select_default_state(port->dev); // 3. 寄存器初始化 serial8250_do_startup(port); // 4. 中断注册 request_irq(port->irq, serial8250_interrupt, IRQF_SHARED,...); } ``` - 通过`serial8250`标准驱动完成底层初始化 - 与裸机开发不同,寄存器操作被封装在驱动中 --- ### **二、用户空间调用场景** 1. **设备文件访问** ```bash # 查看已注册UART设备 ls /dev/ttyS* # 物理串口 ls /dev/ttyUSB* # USB转串口 ``` - 通过设备文件(如`/dev/ttyS0`)进行操作 2. **配置工具使用** ```bash stty -F /dev/ttyS0 115200 cs8 -parenb -cstopb ``` - 使用`stty`设置波特率、数据位等参数 - 配置等效于设置`termios`结构体: ```c struct termios tty; tcgetattr(fd, &tty); cfsetospeed(&tty, B115200); tty.c_cflag &= ~CSIZE; tty.c_cflag |= CS8; ``` 3. **系统服务调用** - getty服务管理串口登录: ```bash systemctl start serial-getty@ttyS0.service ``` - udev规则自动配置: ```bash SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", MODE="0666" ``` --- ### **三、典型调试场景** 1. **内核启动日志输出** ```bash dmesg | grep tty # 输出示例:console [ttyS0] enabled ``` - 通过`earlycon`或`console=`参数指定调试串口 2. **用户空间测试** ```bash echo "test" > /dev/ttyS0 # 发送数据 cat < /dev/ttyS0 # 接收数据 ``` - 需注意权限问题(用户需加入`dialout`组) 3. **高级调试工具** ```bash screen /dev/ttyS0 115200 minicom -D /dev/ttyS0 -b 115200 ``` - 实时交互式终端访问 --- ### **四、动态配置与热插拔** 1. **运行时参数修改** ```c struct serial_rs485 rs485_conf; ioctl(fd, TIOCSRS485, &rs485_conf); // 配置RS485模式 ``` 2. **热插拔事件处理** ```bash udevadm monitor --property | grep DEVNAME ``` - 监USB转串口设备的插拔事件 --- ### **五、注意事项** 1. **权限管理** ```bash sudo usermod -aG dialout $USER # 添加用户到串口组 ``` 2. **流配置** ```bash stty -F /dev/ttyS0 crtscts # 启用硬件流 ``` 3. **内核版本差异** - 4.x以上内核使用`serdev`框架管理串行设备 - 旧版本依赖`tty`子系统直接操作 4. **常见错误处理** ```bash dmesg | grep serial # 查看驱动加载错误 strace -e openat cat /dev/ttyS0 # 跟踪系统调用 ``` --- ### **附:典型代码流程(用户空间)** ```c int main() { int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY); // 1. 获取当前配置 struct termios tty; tcgetattr(fd, &tty); // 2. 清除现有标志 tty.c_cflag &= ~(CSIZE | PARENB | CSTOPB); // 3. 设置新参数 tty.c_cflag |= CS8 | CREAD | CLOCAL; cfsetispeed(&tty, B115200); cfsetospeed(&tty, B115200); // 4. 应用配置 tcsetattr(fd, TCSANOW, &tty); // 5. 数据读写 write(fd, "AT\r\n", 4); read(fd, buf, sizeof(buf)); close(fd); return 0; } ```
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值