Learning Linux device driver from scratch -- interrupt and event management

Interrupt and event management

1, Interrupt

  1. Interrupt entry process

    • In order to improve the real-time performance of external time processing, almost all the displayed processors contain interrupt controllers, and the peripherals also have the function of interrupt triggering. In order to support this feature, a terminal subsystem is designed in Linux to manage interrupts in the system.
    • Many processors have interrupt controllers, which are responsible for interrupt management. Let's discuss how to call the interrupt handler in the driver when an interrupt occurs.
    1. Generally, we will write the exception vector table and mark the starting address of the exception vector table. During kernel startup, we will move the exception vector table to the position of 0xFFFF0000. By setting the first off register of the processor, we can remap the exception vector table. When different interrupts occur, the program will jump to the specific position of the previous interrupt vector table to execute the code.
    2. After the terminal processing function is executed, some operations to reply to the site will be carried out. That is, to return to the state of the program before the interrupt execution.
  2. Interrupt handling in driver

    After analyzing the interrupt handling process, let's take a look at how to support interrupts in the driver.

    • Construct the struct object of struct irpaction and add it to the corresponding linked list according to the IRQ number. We can call the following API interfaces provided by the kernel.
    int request_irq(unsigned int irq,irq_handler handler,unsigned long flags,const char *name,void *dev);
    @irq :Used on the device IRQ No. this number is not found in the hardware manual, but in the kernel IRQ No. this number will be used to determine the struct irqaction Object is inserted into which linked list and used for initialization strut irqaction In object handler Members.
    @handler :Pointer to the interrupt handler,The types are defined as follows
    irqreturn_t (*irq_handler_t)(int ,void*)
        @The interrupt handler function returns an enum of type irqreturn_t Examples of enumeration values, such as
        	IRQ_NONE: Interrupts that are not generated by driving the managed devices are used to share interrupts
        	IRQ_HANDLED: Interrupt is handled normally
        	IRQ_WAKE_THREAD: Need to wake up a kernel thread
    @flag : Flag bits associated with interrupts for initialization struct irqaction In object flag Members. Common flags are as follows. These flags can be set in bit or.
    	IRQF_TRIGGER_RISING: Rising edge trigger
    	IRQF_TRIGGER_FALLING: Falling edge trigger
    	IRQF_TRIGGER_HIGH: High level trigger
    	IRQF_TRIGGER_LOW: Low level trigger
    	IRQF_DISABLED: Interrupt function is forbidden during execution and will be discarded
    	IRQF_SHARED: Flag that must be set for sharing interrupt
    	IRQF_TIMER: Timer specific interrupt flag
    @name: The interrupt is in/proc The name in, used for initial a hu struct irqaction Object name Members.
    @dev: Different devices that share interrupts struct irqaction Objects, in struct irqaction Required when the object is removed from the linked list.
    Return value: request_irq The function returns 0 successfully and a negative value if it fails.
    Note: this function constructs a struct irqaction Object and added to the corresponding linked list, and the corresponding interrupt is enabled.
    • Unregister an interrupt handler
    void free_irq(unsigned int,void *);
    @Parameter 1: IRQ No
    @Parameter 2: dev_id,Shared interrupt must pass a non NULL Arguments to, and request_irq Medium dev_id Consistent.
    • In addition to these, the kernel also provides functions or macros for interrupt enabling and disabling, which are not commonly used.
    local_irq_enable(): Enable local CPU Interrupt for
    local_irq_disable(): Prohibit local CPU interrupt
    local_irq_save(flags): Enable local CPU Interrupt and save the previous interrupt enable status in the flags Medium.
    local_irq_restore(flags): use flags The interrupt enable status in recovers the interrupt enable flag.
    void enable_irq(unsigned int irq): Enable irq Specified interrupt.
    void disable_irq(unsigned int irq): Synchronization inhibit irq Specified interrupt, i.e. wait until irq Interrupts cannot be disabled until all interrupt handlers on the are completed. Obviously, you cannot call in an interrupt handler.
    void disable_irq_nosync(unsigned irq): Immediate prohibition irq Specified interrupt.
    • The interrupt handler function should complete quickly and should not take too long. Because interrupts are forbidden during the whole process of interrupt processing, if the execution time of the interrupt processing function is too long, other interrupts will be suspended. This will have a serious impact on the corresponding of other interrupts.
    • In addition, we need to remember that the scheduler must not be called in interrupt handling functions, that is, functions that may cause thread switching must not be called. (because once the interrupt handler is switched, it cannot be scheduled again.) this is a strict restriction on interrupt handler functions by the kernel. For example, our common copy_from_user, copy_to_user will cause process switching.

2, Interrupt bottom half

  • We mentioned earlier that the interrupt handling function should be completed as soon as possible, otherwise it will affect the timely response to other interrupts, thus affecting the performance of the whole system. But sometimes these time-consuming operations may not be avoided. Take the network card as an example. When the network card receives a data, it will generate an interrupt. In the interrupt processing program, it is necessary to copy the data from the cache of the network card, and then strictly check the data packet. After the check, it will unpack the data packet according to the protocol, and finally deliver the unpacked data packet to the upper layer. If new data is received during this process, the interrupt will be generated again. Because the previous interrupt processing is in the process of processing, the new interrupt will be suspended, and the newly received data will not be processed in time. Because the buffer size of the network card is limited, if more packets arrive later, they will eventually overflow, resulting in packet loss. So what should we do when this happens?
  • Linux divides interrupts into two parts, the top half and the bottom half (top half and bottom half). They are respectively used to complete the following tasks:
    • Top half: complete urgent but quick tasks
    • Lower part: complete non urgent but time-consuming operations
  • In the example of the network card mentioned above, the upper half of the interrupt is mainly used to copy the data from the cache of the network card to the memory, which is an urgent but fast thing that needs to be put into the upper half for execution. The lower half is used to complete the non urgent but time-consuming operations such as checking and disassembling the package.
  • In addition, note: * * during the execution of the lower half, the interrupt is re enabled. Therefore, if a new hardware interrupt is generated, the execution of the program in the lower half will be stopped and the upper half of the hardware interrupt will be executed** This avoids the problem that the interrupt response is not timely.
  1. Soft interrupt

    Although the implementation of the second half can be postponed, we still hope that it can be implemented as soon as possible. So when can I try the second half? It must be after the implementation of the first half. That is to say, the earliest execution time of the second half of the interrupt is after the execution of the interrupt is completed, but the interrupt has not fully returned.

    Soft interrupt is one of the lower half mechanisms of interrupt. The structure describing soft interrupt is struct soft action. Its definition is very simple, that is, a function pointer is embedded. The kernel defines NR_ Softiros (ten in total at present), struct softirq_action object. The kernel has an integer global integer variable to record whether there are corresponding soft interrupts to be executed.

    It can respond to new hardware interrupts during the execution of soft interrupts, which means that soft interrupts are suitable to be used to implement the lower half of the interrupt mechanism.

    Although soft interrupts can be used to implement the mechanism of the lower half of interrupts, soft interrupts are basically pre-defined by kernel developers. They are usually used in occasions with high performance requirements, and require some kernel programming skills, which is not suitable for driver developers.

  2. tasklet

    As mentioned earlier, soft interrupts are usually designed by kernel developers, but kernel developers have reserved soft interrupts for driver developers, which is TASKLET_SOFTIRQ, corresponding soft interrupt handling functional tasklet_action. Take the kernel version 2.6 as an example, in /kernel/softirq The following code is extracted from c/

    static void tasklet_action(struct softirq_action *a)
        struct tasklet_struct *list;
        list = __this_cpu_read(tasklet_vec.head);
            struct tasklet_struct *t = list;
            list = list->next;

    That is, during the processing of soft interrupts, if tasklet_ If the bit corresponding to softirq is set, according to the previous analysis, tasklet_ The action function will be called. In line 5 of the code, we first get a struct tasklet_struct object, and then traverse the list, call the function pointed to by the func member, and pass the data member as a parameter. Data is the parameter passed to the lower half of the function.

    So we can see that we need to think about struct tasklet_struct structure object, initialize its members, put them into the tasklet linked list of the corresponding CPU, and finally set the soft interrupt number tasklet_ The bit corresponding to softirq.

    struct tasklet_struct
        struct tasklet tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;

    Summarize the tasklet:

    1. tasklet is a specific soft interrupt in the interrupt context
    2. Tasklet_ After the schedule function is called, the corresponding lower part is guaranteed to be executed at least once.
    3. If a tasklet has been scheduled but not yet executed, the new schedule will be ignored.
  3. Work queue

    The mechanisms in the lower half of interrupts mentioned earlier, such as soft interrupts and tasklet s, have a limitation that the scheduler cannot be called directly or indirectly when executing in the context of interrupts. To solve this problem, the kernel provides another lower half mechanism called work queue.

    **Work queue: * * create one or more (multi-core processor) kernel worker threads when the kernel is started. The worker thread takes out each work in the work queue and executes it. When there is no work in the queue, the worker thread sleeps.

    Workflow: when the driver wants to delay the execution of a work, it constructs a work queue node object, then joins the corresponding work queue and wakes up the worker thread. The worker thread takes out the nodes on the work queue to complete the work, and then sleeps after all the work is completed. Because it runs in a process context, the work can invoke the scheduler. Work queues provide a mechanism for delaying execution. Obviously, this mechanism is also applicable to the lower half of the terminal.

    In addition to the work queues of the kernel itself, we can also use the kernel infrastructure to create our own work queues. The following is the structure type definition of the work queue node.

    struct work_struct{
        stomic_long_t data;
        struct list_head entry;
        work_func_t func;
    @data: Parameters passed to the work queue are usually integers, but pointers are more commonly used.
    @entry: Linked list node objects that make up the work queue
    @func: The work function is executed after the worker thread takes out the work queue node, data Will be used as an argument to call the function.

    Summary of work queues:

    1. The work function of the work pair runs in the process context and can schedule the scheduler.
    2. If the previous work has not been completed and the next work is rescheduled, the new work will not be scheduled.
  4. Time delay control

    Time delay is often used in hardware operation, such as how long to keep the chip reset time, chip power on timing control, etc. A set of delay operation functions is provided for this kernel.

    The kernel sub calculates a global loops during startup_ Per_ The value of jiffy. This variable reflects how many times the code with a loop delay needs to loop to delay a jiffy. According to the value of jiffy, you can know how many cycles it takes to delay a minute and how many cycles it takes to delay a millisecond. So the kernel defines some macros or functions that are delayed by loops. For example:

    void ndelay(unsigned long x); //Nanosecond delay
    udelay(n);	//Microsecond delay
    mdelay(n);	//Millisecond delay

    These delay functions are busy waiting delays, which are obtained by consuming CPU time in vain. If there is no special reason (such as obtaining spin lock in interrupt context), it is not recommended to use these functions to delay for a long time.

    Sleep delay is recommended as follows:

    void msleep(unsigned int msecs);	//Hibernation cannot be interrupted by signals. You can only return when the hibernation time is up
    long msleep_interuptible(unsigned int msecs);	//Sleep can be interrupted by signals
    void ssleep(unsigned int seconds);	//Hibernation cannot be interrupted by signals. You can only return when the hibernation time is up
  5. Timed operation

    Sometimes we need to perform an operation automatically at the set time, which is called timing. Timing can be divided into single timing and cycle timing. The so-called single timing means that the operation is performed once after the set time expires. The cycle timing is that the operation is executed after the set time expires, and then the timer is started again. The operation is executed after the next time expires, and then the timer is started again. This cycle repeats. At present, there are low resolution timer and high resolution timer in Linux.

    Low resolution timer

    • The classic timer is based on a hardware timer. The timer periodically generates interrupts. The number of interrupts can be configured. For example, we can set how many interrupts are generated per second. The number of interrupts generated by the timer since it was started is recorded in the jiffies global variable.

    • To implement a timer in the driver, you need to go through the following steps.

      1. Construct a timer object and call init_timer to initialize this object and assign values to the expires, function, and data members.
      struct timer_list{
          struct list_head entry;
          unsigned long expires;
          struct tvec_base *base;
          void (*function)(unsigned long)
          unsigned long data;
      @entry: The object of a bidirectional linked list node, which is used to form a bidirectional linked list.
      @ecpires: Timer expired jiffies Value.
      @function: Functions executed after timer expires
      @data: Pass the parameters of the timer function, usually a pointer
      1. Using add_timer adds the timer object to the timer linked list of the kernel.
      inti_timer(timer);	//Initialize a timer
      void add_timer(struct timer_list *timer);
      //Add the timer to the timer linked list in the kernel.
      int mod_timer(struct timer_list *timer,unsigned long expires);
      //Modify the expires member of the timer, regardless of the status of the current timer
      int del_timer(struct timer_list *timer);
      //Delete the timer from the kernel linked list, regardless of the status of the current timer
      1. When the timing time is up, the timer function will be called automatically. If periodic timing is required, mod can be used in the timing function_ Timer to modify expires.
      2. When a timer is not required, use del_timer to delete the timer.

      The kernel handles these timers in the lower half of the soft interrupt of timer interrupt. So the timer is executed in the interrupt context.

    high-resolution timer

    • In the previous main, the low resolution slave timer is timed by jiffies, so the timing accuracy is affected by the system setting. For example, if we set 200 interrupts as 1 second, the time of a jiffy is 5 milliseconds, that is, the accuracy of the timer is 5 milliseconds.
    • For the equipment with high time requirements, this accuracy is obviously not satisfied, such as sound card. To this end, the kernel has developed the semantic ktime_t to define the time. The types are defined as follows:
    union ktime{
        s64 tv64;
    #if BITS_PER_LONG != 64 && !defined(CONFIG_KTIME_SCALAR)
    # ifdef __BIG_ENDIAN
            S32 sec,nsec;
    # endif
    typedef union ktime ktime_t;
    • ktime is a common body. It can be seen that it can be accurate to nanoseconds (nsec).
    • General ktime_set function to initialize this object. Common methods are as follows:
    ktime_t t = ktime_set(secs,nsecs);
    • The structure type of the resolution timer is defined as follows.
    struct hrtimer{
        struct timerqueue_node	node;
        ktime_t	_softexpires;
        enum hrtimer_restart	(*function)(struct hrtime *);
        struct hrtimer_clock_base	*base;
        unsigned	state;
    //Where function is the function to be executed when the fixed time expires.

Posted by DoctorT on Tue, 31 May 2022 01:17:35 +0530