Linux 内核源代码情景分析(一)(上)

简介: Linux 内核源代码情景分析(一)

一、存储管理

1、外部设备存储空间的地址映射

   任何系统都免不了要有输入/ 输出,所以对外部设备的访问是 CPU 设计中的一个重要问题。一般来说,对外部设备的访问有两种不同的形式,一中叫内存映射式 (memory mapped),另一种叫 I/O 映射式 (I/O mapped)。在采用内存映射方式的 CPU 中,外部设备的存储单元,如控制寄存器、状态寄存器、数据寄存器等等,去作为内存的一部分出现在系统中的。CPU 可以像访问一个内存单元一样地访问外部设备的存储单元,所以不需要专门设立用于外设 I/O 的指令。从前的 PDP-11、后来的 M68K、Power PC 等 CPU 都采用这种方式。而在采用 I/O 映射方式的系统中则不同,外部设备的存储单元与内存分属两个不同的体系。访问内存的指令不能用来访问外部设备的存储单元,所以在 X86 CPU 中设立了专门的 IN 和 OUT 指令,但是用于 I/O 指令的 “地址主间” 相对来说是很小的。事实上,现在 X86 的 I/O 地址空间已经非常拥挤。


   但是,随着计算机技术的发展,人们发现单纯的 I/O 映射方式是不能满足要求的。此种方式只适合于早期的计算机技术,那时候一个外设通常都只有几个寄存器,通过这几个寄存器就可以完成对外设的所有操作了。而现在的情况却大不一样。例如,在 PC 机上可以插上一块图像卡,带有 2MB 的存储器,甚至还可能带有一块 ROM,里面装有可执行代码。自从 PCI 总线出现以后,这个问题就更突出了。所以,不管 CPU 的设计采用 I/O 映射或是存储器映射,都必须要有将外设卡上的存储器映射到内存空间,实际上是虚存空间的手段。在 Linux 内核中,这样的映射是通过函数 ioremap() 来建立的。


   对于内存页面的管理,通常我们都是先在虚存空间分配一个虚存区间,然后为此区间分配相应的物理内存页面并建立起映射。而且这样的映射也并不是一次就建立完毕,可以在访问这些虚存页面引起页面异常时逐步地建立。但是,ioremap() 则不同,首先,我们先有一个物理存储区间,其地址就是外设卡上的存储器出现在总线上的地址。这地址未必就是这些存储单元在外设卡上局部的物理地址,而是在总线上由 CPU 所 “看到” 的地址,这中间很可能已经经历了一次地址映射,但这种映射对于 CPU 来说是透明的。所以有时把这种地址称为 “总线地址” 。举例来说,如果有一块 “智能图形卡” ,卡上有个微处理器。对于卡上的微处理器来说,卡上的存储器是从地址 0 开始的,这就是卡上局部的物理地址。但是将这块图形卡插到 PC 的一个 PCI 总线插槽上时,由 PC 的 CPU 所看到的这片物理存储区间的地址可能是从 0x0000 f000 0000 0000 开始的,这中间已经有了一次映射。可是,从系统 (PC) 的 CPU 的角度来说,它只知道这片物理存储区间是从 0x0000 f000 0000 0000 开始的,这就是该区间的物理地址,或者说 “总线地址” 。在 Linux 系统中,CPU 不能按物理地址来访问存储中间,而必须使用虚拟地址,所以必需 “反向” 地从物理地址出发找到一片虚存空间并建立起映射。其次,这样的需求只发生于对外部设备的操作,而这是内核的事,所以相应的虚存区间是在系统空间 (3GB 以上) 。在以前的 Linux 内核版本中,这个函数称为 vremap(),后来改成了 ioremap(),也突出地反映了这一点。还有。这样的页面当然不服从动态的物理内存页面分配,也不服从 kswapd 的换出。

(1)ioremap

// include/asm-i386/io.h
extern inline void * ioremap (unsigned long offset, unsigned long size)
{
  return __ioremap(offset, size, 0);
}

// arch/i386/mm/ioremap.c
/*
 * Remap an arbitrary physical address space into the kernel virtual
 * address space. Needed when the kernel wants to access high addresses
 * directly.
 *
 * NOTE! We need to allow non-page-aligned mappings too: we will obviously
 * have to convert them into an offset in a page-aligned mapping, but the
 * caller shouldn't need to know that small detail.
 */
void * __ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags)
{
  void * addr;
  struct vm_struct * area;
  unsigned long offset, last_addr;

  /* Don't allow wraparound or zero size */
  last_addr = phys_addr + size - 1;
  if (!size || last_addr < phys_addr)
    return NULL;

  /*
   * Don't remap the low PCI/ISA area, it's always mapped..
   */
  if (phys_addr >= 0xA0000 && last_addr < 0x100000)
    return phys_to_virt(phys_addr);

  /*
   * Don't allow anybody to remap normal RAM that we're using..
   */
  if (phys_addr < virt_to_phys(high_memory)) {
    char *t_addr, *t_end;
    struct page *page;

    t_addr = __va(phys_addr);
    t_end = t_addr + (size - 1);
     
    for(page = virt_to_page(t_addr); page <= virt_to_page(t_end); page++)
      if(!PageReserved(page))
        return NULL;
  }

  /*
   * Mappings have to be page-aligned
   */
  offset = phys_addr & ~PAGE_MASK;
  phys_addr &= PAGE_MASK;
  size = PAGE_ALIGN(last_addr) - phys_addr;

  /*
   * Ok, go for it..
   */
  area = get_vm_area(size, VM_IOREMAP);
  if (!area)
    return NULL;
  addr = area->addr;
  if (remap_area_pages(VMALLOC_VMADDR(addr), phys_addr, size, flags)) {
    vfree(addr);
    return NULL;
  }
  return (void *) (offset + (char *)addr);
}

   首先是一些例行检查,常常称为 “sanity check”,或者说 “健康检查”、“卫生检查”。其中 109 行检查的是区间的大小既不为 0,也不能太大而越出了 32 位地址空间的限制。物理地址 0xa0000 至 0x100000 用于 VGA 卡和 BIOS,这是在系统初始化时就映射好了的,不能侵犯到这个区间中去。121 行中的 high_memory 是在系统初始化时,根据检测到的物理内存大小设置的物理内存地址的上限 (所对应的虚拟地址)。如果所要求的 phys_addr 小于这个上限的话,就表示与系统的物理内存有冲突了,除非相应的物理页面原来就是保留着的空洞。在通过这些检查以后,还要保证该物理地址是按页面边界对齐的 (136~138 行)。


   完成了这些准备以后,这才 “言归正传” 。首先是要找到一片虚存地址区间。前面讲过,这片区间属于内核,而不属于任何一个特定的进程,所以不是在某个进程的 mm_struct 结构中的虚存区间队列中去寻找,而是从属于内核的虚存区间队列中去寻找。

(2)get_vm_area

// mm/vmalloc.c
struct vm_struct * get_vm_area(unsigned long size, unsigned long flags)
{
  unsigned long addr;
  struct vm_struct **p, *tmp, *area;

  area = (struct vm_struct *) kmalloc(sizeof(*area), GFP_KERNEL);
  if (!area)
    return NULL;
  size += PAGE_SIZE;
  addr = VMALLOC_START;
  write_lock(&vmlist_lock);
  for (p = &vmlist; (tmp = *p) ; p = &tmp->next) {
    if ((size + addr) < addr) {
      write_unlock(&vmlist_lock);
      kfree(area);
      return NULL;
    }
    if (size + addr < (unsigned long) tmp->addr)
      break;
    addr = tmp->size + (unsigned long) tmp->addr;
    if (addr > VMALLOC_END-size) {
      write_unlock(&vmlist_lock);
      kfree(area);
      return NULL;
    }
  }
  area->flags = flags;
  area->addr = (void *)addr;
  area->size = size;
  area->next = *p;
  *p = area;
  write_unlock(&vmlist_lock);
  return area;
}

    内核为自己保持一个虚存区间队列 vmlist,这是由一串 vm_struct 数据结构组成的一个单链队列。这里的 vm_structvmlist 都是由内核专用的。vm_struct 从概念上说类似于供进程使用的 vm_area_struct,但要简单得多,定义于 include/linux/vmalloc.h 和 mm/vmalloc.c 中:

// include/linux/vmalloc.h
struct vm_struct {
  unsigned long flags;
  void * addr;
  unsigned long size;
  struct vm_struct * next;
};

    以前讲过,内核使用的系统空间虚拟地址与物理地址间存在着一种简单的映射关系,只要在物理地址上加上一个 3GB 的偏移量就得到了内核的虚拟地址。而变量 high_memory 标志着具体物理内存的上限所对应的虚拟地址,这是在系统初始化时设置好的。当内核需要一片虚存地址空间时,就从这个地址以下 8MB 处分配。为此,在 include/asm-i386/pgtable.h 中定义了 VMALLOC_START 等有关的常数:

// include/asm-i386/pgtable.h
/* Just any arbitrary offset to the start of the vmalloc VM area: the
 * current 8MB value just means that there will be a 8MB "hole" after the
 * physical memory until the kernel virtual memory starts.  That means that
 * any out-of-bounds memory accesses will hopefully be caught.
 * The vmalloc() routines leaves a hole of 4kB between each vmalloced
 * area for the same reason. ;)
 */
#define VMALLOC_OFFSET  (8*1024*1024)
#define VMALLOC_START (((unsigned long) high_memory + 2*VMALLOC_OFFSET-1) & \
            ~(VMALLOC_OFFSET-1))
#define VMALLOC_VMADDR(x) ((unsigned long)(x))
#define VMALLOC_END (FIXADDR_START)

   源代码中的注解对于为什么要留一个 8MB 的空洞,以及在每次分配虚存区间时也要留下一个页面的空洞 (见 132 行) 解释得很清楚:是为了便于捕捉可能的越界访问。


   这里读者可能会有个问题,185 行的 if 语句检查的是当前的起始地址加上区间大小须小于下一个区间的起始地址,这是很好理解的。可是176行在区间大小上又加了一个页面作为空洞。这个空洞页面难道不可能与下一个区间的起始地址冲突吗?这里的奥妙在于185行判定的条件是 “<” 而不是 “<=” , 并且 size 和 addr 都是按页面边界对齐的,所以 185 行的条件已经隐含着其中有一个页面的空洞。从 get_vm_area() 成功返回时,就标志着所需要的一片虚存空间已经分配好了,从返回的数据结构可以得到这片空间的起始地址。下面就是建立映射的事了。


   宏定义 VMALLOC_VMADDR 我们已经在前面看到过了,实际上不做什么事情,只是类型转换。 函数 remap_area_pages() 的代码也在 arch/i386/mm/ioremap.c 中:

(3)remap_area_pages

// arch/i386/mm/ioremap.c
static int remap_area_pages(unsigned long address, unsigned long phys_addr,
         unsigned long size, unsigned long flags)
{
  pgd_t * dir;
  unsigned long end = address + size;

  phys_addr -= address;
  dir = pgd_offset(&init_mm, address);
  flush_cache_all();
  if (address >= end)
    BUG();
  do {
    pmd_t *pmd;
    pmd = pmd_alloc_kernel(dir, address);
    if (!pmd)
      return -ENOMEM;
    if (remap_area_pmd(pmd, address, end - address,
           phys_addr + address, flags))
      return -ENOMEM;
    address = (address + PGDIR_SIZE) & PGDIR_MASK;
    dir++;
  } while (address && (address < end));
  flush_tlb_all();
  return 0;
}

   我们讲过,每个进程的 task_struct 结构中都有一个指针指向 mm_strcuct 结构,从中可以找到相应的页面目录。但是,内核空间不属于任何一个特定的进程,所以单独设置了一个内核专用的 mm_strcuct , 称为 init_mm。当然,内核也没有代表它的 task_struct 结构,所以69行根据起始地址从 init_mm 中找到所属的目录项,然后就根据区间的大小走遍所有涉及的目录项。这里的68行看似奇怪。从物理地址中减去虚拟地址得出一个负的位移量,这个位移量在78〜79行又与虚拟地址相加,仍旧得到物理地址。 由于在循环中虚拟地址 address 在变 (见81行),物理地址也就相应而变。第75行的 pmd_alloc_kemel() 对于 i386 CPU 就是 pmd_alloc()。

// include/asm-i386/pgalloc.h
#define pmd_alloc_kernel  pmd_alloc

// ==============================================================================
// include/asm-i386/pgalloc-2level.h
extern inline pmd_t * pmd_alloc(pgd_t *pgd, unsigned long address)
{
  if (!pgd)
    BUG();
  return (pmd_t *) pgd;
}

   可见,对于i386的二级页式映射,只是把页面目录项当成中间目录项而已,与“分配”实际上毫无关系。即使对于采用了物理地址扩充(PAE)的Pentium CPU,虽然实现三级映射,其作用也只是“找到”中间目录项而已,只有在中间目录项为空时才真的分配一个。


   这样,remap_area_pages() 中从73行开始的do_while循环,对涉及到的每个页面目录表项调用 remap_area_pmd( )。而 remap_area_pmd() 几乎完全一样,对涉及到的每个页面表 (对i386的二级映射, 每个中间目录项实际上就是一个页面表项,也可以理解为中间目录表的大小为1)

(3)remap_area_pte

// arch/i386/mm/ioremap.c
static inline void remap_area_pte(pte_t * pte, unsigned long address, unsigned long size,
  unsigned long phys_addr, unsigned long flags)
{
  unsigned long end;

  address &= ~PMD_MASK;
  end = address + size;
  if (end > PMD_SIZE)
    end = PMD_SIZE;
  if (address >= end)
    BUG();
  do {
    if (!pte_none(*pte)) {
      printk("remap_area_pte: page already exists\n");
      BUG();
    }
    set_pte(pte, mk_pte_phys(phys_addr, __pgprot(_PAGE_PRESENT | _PAGE_RW | 
          _PAGE_DIRTY | _PAGE_DIRTY  | flags)));
    address += PAGE_SIZE;
    phys_addr += PAGE_SIZE;
    pte++;
  } while (address && (address < end));
}

   这里只是简单地在循环中设置页面表中所有涉及的页面表项(31行)。每个表项都被预设成 _PAGE_DIRTY 、_PAGE_DIRTY 和 _PAGE_PRESENT。


   在 kswapd 换出页面的情景中,我们已经看到 kswapd 定期地、循环地、依次地从 task 结构队列中找出占用内存页面最多的进程,然后就对该进程调用 swap_out_mm() 换出一些页面。而内核的 mm_struct 结构 init_mm 是单独的,从任何一个进程的 task 结构中都到达不了 init_mm 。 所以,kswapd 根本就看不到 init_mm 中的虚存区间,这些区间的页面就自然不会被换出而长驻于内存。

2、系统调用brk()

   尽管“可见度”不高,brk() 也许是最常使用的系统调用了,用户进程通过它向内核申请空间。人们常常并不意识到在调用 brk(),原因在于很少有人会直接使用系统调用 brk() 向系统申请空间,而总是通过像 malloc() 一类的C语言库函数 (或语言成分,如 C++ 中的 new ) 间接地用到 brk()。如果把 malloc() 想像成零售,brk() 则是批发。库函数 malloc() 为用户进程 (malloc本身就是该进程的一部分) 维持一个小仓库,当进程需要使用更多的内存空问时就向小仓库要,小仓库中存量不足时就通过 brk() 向内核批发。


   前面讲过,每个进程拥有 3G 字节的用户虚存空间。但是,这并不意味着用户进程在这 3G 字节的范围里可以任意使用,因为虚存空间最终得映射到某个物理存储空间 (内存或磁盘空间) ,才真正可以使用,而这种映射的建立和管理则由内核处理。所谓向内核申请一块空间,是指请求内核分配一块虚存区间和相应的若干物理页面,并建立起映射关系。由于每个进程的虚存空间都很大 (3G),而实际需要使用的又很小,内核不可能在创建进程时就为整个虚存空间都分配好相应的物理空间并建立映射, 而只能是需要用多少才“分配”多少。


   那么,内核怎样管理每个进程的3G字节虚存空间呢?粗略地说,用户程序经过编译、连接形成的映象文件中有一个代码段和一个数据段 (包括 data 段和 bss 段),其中代码段在下,数据段在上。数据段中包括了所有静态分配的数据空间,包括全局变量和说明为 static 的局部变量。这些空间是进程所必须的基本要求,所以内核在建立一个进程的运行映象时就分配好这些空间,包括虚存地址区间和物理页面,并建立好二者间的映射。除此之外,堆栈使用的空间也属于基本要求,所以也是在建立进程时就分配好的(但可以扩充)。所不同的足,堆栈空间安置在虚存空间的顶部,运行时由顶向下延伸;代码段和数据段则在底部 (注意,不要与X86系统结构中由段寄存器建立的“代码段”及“数据段”相混淆),在运行时并不向上伸展。而从数据段的顶部 end-data 到堆栈段地址的下沿这个中间区域则是一个巨大的空洞,这就是可以在运行时动态分配的空间。最初,这个动态分配空间是从进程的 end_data 开始的,这个地址为内核和进程所共知。以后,每次动态分配一块“内存”,这个边界就往上推进一段 距离,同时内核和进程都要记下当前的边界在哪里。在进程这一边由 malloc() 或类似的库函数管理, 而在内核中则将当前的边界记录在进程的 mm_struct 结构中。具体地说,mm_struct 结构中有一个成分 brk,表示动态分配区当前的底部。当个进程需要分配内存时,将要求的大小与其当前的动态分配区底部边界相加,所得的就是所要求的新边界,也就是 brk() 调用时的参数 brk。当内核能满足要求时, 系统调用 brk() 返回 0 ,此后新旧两个边界之间的虚存地址就都可以使用了。当内核发现无法满足要求 (例如物理空间已经分配完),或者发现新的边界已经过于逼近设于顶部的堆栈时,就拒绝分配而返回 -1 。


   系统调用 brk() 在内核中的实现为 sys_brk(),其代码在 mm/mmap.c 中。这个函数既可以用来分配空间,即把动态分配区底部的边界往上推;也可以用来释放,即归还空间。因此,它的代码也大致上可以分成两部分。

// mm/mmap.c
/*
 *  sys_brk() for the most part doesn't need the global kernel
 *  lock, except when an application is doing something nasty
 *  like trying to un-brk an area that has already been mapped
 *  to a regular file.  in this case, the unmapping will need
 *  to invoke file system routines that need the global lock.
 */
asmlinkage unsigned long sys_brk(unsigned long brk)
{
  unsigned long rlim, retval;
  unsigned long newbrk, oldbrk;
  struct mm_struct *mm = current->mm;

  down(&mm->mmap_sem);

  if (brk < mm->end_code)
    goto out;
  newbrk = PAGE_ALIGN(brk);
  oldbrk = PAGE_ALIGN(mm->brk);
  if (oldbrk == newbrk)
    goto set_brk;

  /* Always allow shrinking brk. */
  if (brk <= mm->brk) {
    if (!do_munmap(mm, newbrk, oldbrk-newbrk))
      goto set_brk;
    goto out;
  }

  /* Check against rlimit.. */
  rlim = current->rlim[RLIMIT_DATA].rlim_cur;
  if (rlim < RLIM_INFINITY && brk - mm->start_data > rlim)
    goto out;

  /* Check against existing mmap mappings. */
  if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
    goto out;

  /* Check if we have enough memory.. */
  if (!vm_enough_memory((newbrk-oldbrk) >> PAGE_SHIFT))
    goto out;

  /* Ok, looks good - let it rip. */
  if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
    goto out;
set_brk:
  mm->brk = brk;
out:
  retval = mm->brk;
  up(&mm->mmap_sem);
  return retval;
}

   参数 brk 表示所要求的新边界,这个边界不能低于代码段的终点,并且必须与页面大小对齐。如 果新边界低于老边界,那就不是申请分配空间,而是释放空间,所以通过 do_munmap() 解除一部分区间的映射。


   首先检查对进程的资源限制,如果所要求的新边界使数据段的大小超过了对当前进程的限制,就拒绝执行。此外,还要通过 find_vma_intersection(),检查所要求的那部分空间是否与已经存在的某一区间相冲突。

二、中断、异常和系统调用

1、中断请求队列的初始化

   在前一节中,我们讲到中断向量表(更准确地,应该说“中断描述表”)IDT 中有两种表项,一种是为保留专用于 CPU 本身的中断门,主要用于由CPU产生的异常,如“除数为0”、“页面错”等等, 以及由用户程序通过 INT 指令产生的中断(或称“陷阱”),主要用来产生系统调用(另外还有个用于 debug 的 INT3)。这些中断门的向量除用于系统调用的 0x80 外都在 0x20 以下。从 0x20 开始就是第 2 种表项,共 224 项,都是用于外设的通用中断门。这二者的区别在于通用中断门可以为多个中断源所共享,而专用中断门则是为特定的中断源所专用。


   由于通用中断门是让多个中断源共用的,而且允许这种共用的结构在系统运行的过程中动态地变化,所以在 IDT 的初始化阶段只是为每个中断向量,也即每个表项准备下一个“中断请求队列”,从而形成一个中断请求队列的数组,这就是数组 irq_desc 。

(1)irq_desc_t

// include/linux/irq.h
/*
 * Interrupt controller descriptor. This is all we need
 * to describe about the low-level hardware. 
 */
struct hw_interrupt_type {
  const char * typename;
  unsigned int (*startup)(unsigned int irq);
  void (*shutdown)(unsigned int irq);
  void (*enable)(unsigned int irq);
  void (*disable)(unsigned int irq);
  void (*ack)(unsigned int irq);
  void (*end)(unsigned int irq);
  void (*set_affinity)(unsigned int irq, unsigned long mask);
};

typedef struct hw_interrupt_type  hw_irq_controller;

/*
 * This is the "IRQ descriptor", which contains various information
 * about the irq, including what kind of hardware handling it has,
 * whether it is disabled etc etc.
 *
 * Pad this out to 32 bytes for cache and indexing reasons.
 */
typedef struct {
  unsigned int status;    /* IRQ status */
  hw_irq_controller *handler;
  struct irqaction *action; /* IRQ action list */
  unsigned int depth;   /* nested irq disables */
  spinlock_t lock;
} ____cacheline_aligned irq_desc_t;

extern irq_desc_t irq_desc [NR_IRQS];

(2)hw_interrupt_type

   每个队列头部中除指针 action 用来维持一个由中断服务程序描述项构成的单链队列外,还有个指针 handler 指向另一个数据结构,即 hw_interrupt_type 数据结构。那里主要是一些函数指针,用于该队列,或者说该共用“中断通道”的控制(而并不是对具体中断源的服务)。具体的函数则取决于所用的中断控制器(通常是i8259A)。例如,函数指针 enable 和 disable 用来开启和关断其所属的通道,ack 用于对中断控制器的响应,而 end 则用于每次中断服务返回的前夕。这些函数都是在 init_IRQ() 中调用 init_ISA_irqs() 设置好的。

// arch/i386/kernel/i8259.c
void __init init_ISA_irqs (void)
{
  int i;

  init_8259A(0);

  for (i = 0; i < NR_IRQS; i++) {
    irq_desc[i].status = IRQ_DISABLED;
    irq_desc[i].action = 0;
    irq_desc[i].depth = 1;

    if (i < 16) {
      /*
       * 16 old-style INTA-cycle interrupts:
       */
      irq_desc[i].handler = &i8259A_irq_type;
    } else {
      /*
       * 'high' PCI IRQs filled in on demand
       */
      irq_desc[i].handler = &no_irq_type;
    }
  }
}

void __init init_IRQ(void)
{
  // ...
  init_ISA_irqs();
  // ...
}

(3)irqaction

    用于具体中断服务程序描述项的数据结构 irqaction, 则是在 include/linux/interrupt.h 中定义的:

// include/linux/interrupt.h
struct irqaction {
  void (*handler)(int, void *, struct pt_regs *);
  unsigned long flags;
  unsigned long mask;
  const char *name;
  void *dev_id;
  struct irqaction *next;
};

   其中最主要的就是函数指针 handler,指向具体的中断服务程序。


   在 IDT 表的初始化完成之初,每个中断服务队列都是空的。此时即使打开中断并且某个外设中断真的发生了,也得不到实际的服务。虽然从中断源的硬件以及中断控制器的角度来看似乎已经得到服务了,因为形式上 CPU 确实通过中断门进入了某个中断向量的总服务程序,例如IRQ0x01_interrupt() , 并且按要求执行了对中断控制器的 ack() 以及 end() ,然后执行iret 指令从中断返网。但是,从逻辑的角度、功能的角度来看,则其实并没有得到实质的服务,因为并没有执行具体的中断服务程序。所以, 真正的中断服务要到具体设备的初始化程序将其中断服务程序通过 request_irq() 向系统 “登记”,挂入某个中断请求队列以后才会发生。

(3)request_irq

    函数 request_irq() 的代码在 arch/i386/kemel/irq.c 中:

// arch/i386/kernel/irq.c
/**
 *  request_irq - allocate an interrupt line
 *  @irq: Interrupt line to allocate
 *  @handler: Function to be called when the IRQ occurs
 *  @irqflags: Interrupt type flags
 *  @devname: An ascii name for the claiming device
 *  @dev_id: A cookie passed back to the handler function
 *
 *  This call allocates interrupt resources and enables the
 *  interrupt line and IRQ handling. From the point this
 *  call is made your handler function may be invoked. Since
 *  your handler function must clear any interrupt the board 
 *  raises, you must take care both to initialise your hardware
 *  and to set up the interrupt handler in the right order.
 *
 *  Dev_id must be globally unique. Normally the address of the
 *  device data structure is used as the cookie. Since the handler
 *  receives this value it makes sense to use it.
 *
 *  If your interrupt is shared you must pass a non NULL dev_id
 *  as this is required when freeing the interrupt.
 *
 *  Flags:
 *
 *  SA_SHIRQ    Interrupt is shared
 *
 *  SA_INTERRUPT    Disable local interrupts while processing
 *
 *  SA_SAMPLE_RANDOM  The interrupt can be used for entropy
 *
 */
 
int request_irq(unsigned int irq, 
    void (*handler)(int, void *, struct pt_regs *),
    unsigned long irqflags, 
    const char * devname,
    void *dev_id)
{
  int retval;
  struct irqaction * action;

#if 1
  /*
   * Sanity-check: shared interrupts should REALLY pass in
   * a real dev-ID, otherwise we'll have trouble later trying
   * to figure out which interrupt is which (messes up the
   * interrupt freeing logic etc).
   */
  if (irqflags & SA_SHIRQ) {
    if (!dev_id)
      printk("Bad boy: %s (at 0x%x) called us without a dev_id!\n", devname, (&irq)[-1]);
  }
#endif

  if (irq >= NR_IRQS)
    return -EINVAL;
  if (!handler)
    return -EINVAL;

  action = (struct irqaction *)
      kmalloc(sizeof(struct irqaction), GFP_KERNEL);
  if (!action)
    return -ENOMEM;

  action->handler = handler;
  action->flags = irqflags;
  action->mask = 0;
  action->name = devname;
  action->next = NULL;
  action->dev_id = dev_id;

  retval = setup_irq(irq, action);
  if (retval)
    kfree(action);
  return retval;
}

  参数 irq 为中断请求队列的序号,也就是人们通常所说的 “中断请求号”,对应于中断控制器中的 一个通道,有时候要在接口卡上通过微型开关或跳线来设置。但是要注意,这样的中断请求号与 CPU 所用的 “中断号” 或 “中断向量” 是不同的,中断请求号 IRQ0 相当于中断向量0x20。也许,可以把这种中断请求号看成“逻辑”中断向量,而后者则为“物理”中断向量。通常,前 16 个中断请求通道 IRQ0 至 IRQ15 是由中断控制器 i8259A 控制的。参数 irqflags 是一些标志位,其中的 SA_SHIRQ 标志表示与其他中断源公用该中断请求通道。此时必须提供一个非零的 dev_id 以供区别。当中断发生时, 参数 dev_id 会被作为调用参数传回所指定的服务程序。至于这 dev_id 到底是什么,request_irq() 和中断服务的总控并不在乎,只要各个具体的中断服务程序自己能够辨识和使用即可,所以这里 dev_id 的类型为 void * 。而 request_irq() 中则对此进行检查。顺便提一下,printk() 产生一个出错信息,通常是写入文件 /var/log/messages 或者在屏幕上显示,取决于"守护神" syslogd 和 klogd 是否已经在运行。这里有趣的是语句中的参数 (&irq)[-1]。这里irq 是第一个调用参数,所以是最后压入堆栈的,&irq 就是参数 irq 在堆栈中的位置。那么,在 &irq 下面的是什么呢?那就是函数的返回地址。所以,这个 printk() 语句显示该 request_irq() 函数是从什么地方调用的,使程序员可以根据这个地址发现是在哪个函数中调用的。

(4)setup_irq

    在分配并设置了一个 irqaclion 数据结构 action 以后,便调用 setup_irq() ,将其链入相应的中断请求队列。

/* this was setup_x86_irq but it seems pretty generic */
int setup_irq(unsigned int irq, struct irqaction * new)
{
  int shared = 0;
  unsigned long flags;
  struct irqaction *old, **p;
  irq_desc_t *desc = irq_desc + irq;

  /*
   * Some drivers like serial.c use request_irq() heavily,
   * so we have to be careful not to interfere with a
   * running system.
   */
  if (new->flags & SA_SAMPLE_RANDOM) {
    /*
     * This function might sleep, we want to call it first,
     * outside of the atomic block.
     * Yes, this might clear the entropy pool if the wrong
     * driver is attempted to be loaded, without actually
     * installing a new handler, but is this really a problem,
     * only the sysadmin is able to do this.
     */
    rand_initialize_irq(irq);
  }

  /*
   * The following block of code has to be executed atomically
   */
  spin_lock_irqsave(&desc->lock,flags);
  p = &desc->action;
  if ((old = *p) != NULL) {
    /* Can't share interrupts unless both agree to */
    if (!(old->flags & new->flags & SA_SHIRQ)) {
      spin_unlock_irqrestore(&desc->lock,flags);
      return -EBUSY;
    }

    /* add new interrupt at end of irq queue */
    do {
      p = &old->next;
      old = *p;
    } while (old);
    shared = 1;
  }

  *p = new;

  if (!shared) {
    desc->depth = 0;
    desc->status &= ~(IRQ_DISABLED | IRQ_AUTODETECT | IRQ_WAITING);
    desc->handler->startup(irq);
  }
  spin_unlock_irqrestore(&desc->lock,flags);

  register_irq_proc(irq);
  return 0;
}

   计算机系统在使用中常常有产生随机数的要求,但是要产生真正的随机数是不可能的 (所以由计算机产生的随机数称为“伪随机数为了达到尽可能的随机,需要在系统的运行中引入一些随机的因素,称为“熵”(entropy)。由各种中断源产生的中断请求在时间上大多是相当随机的,可以用来作为这样的随机因素。所以Linux内核提供了一种手段,使得可以根据中断发生的时间来引入一点随机 性。需要在某个中断请求队列,或者说中断请求通道中引入这种随机性时,可以在调用参数 irqflags 中将标志位 SA_SAMPLE_RANDOM 设成1。而这里调用的 rand_initialize_irq() 就据此为该中断请求队列初始化一个数据结构,用来记录该中断的时序。


   可想而知,对于中断请求队列的操作当然不允许受到干扰,必须要在临界区内进行,不光中断要关闭,还要防止可能来自其他处理器的干扰。代码中986行的 spin_lock_irqsave() 就使 CPU 进入了这样的临界区。我们将在本书下册“多处理器SMP结构”一章中介绍和讨论 spin_lock_irqsave() ,与之相对的 spin_unlock_irqrestore() 则是临界区的出口。


   对第一个加入队列的 irqaction 结构的处理比较简单(1003行),不过此时要对队列的头部进行一些初始化(1006〜1008行),包括调用本队列的 startup 函数。对于后来加入队列的irqaction 结构则要稍加检查,检查的内容为是否允许共用一个中断通道,只有在新加入的结构以及队列中的第一个结构都允许共用时才将其链入队列的尾部。


   在内核中,设备驱动程序一般都要通过 request_irq() 向系统登记其中断服务程序。

Linux 内核源代码情景分析(一)(中):https://developer.aliyun.com/article/1597929

目录
相关文章
|
3天前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
19 4
|
7天前
|
缓存 算法 Linux
深入理解Linux内核调度器:公平性与性能的平衡####
真知灼见 本文将带你深入了解Linux操作系统的核心组件之一——完全公平调度器(CFS),通过剖析其设计原理、工作机制以及在实际系统中的应用效果,揭示它是如何在众多进程间实现资源分配的公平性与高效性的。不同于传统的摘要概述,本文旨在通过直观且富有洞察力的视角,让读者仿佛亲身体验到CFS在复杂系统环境中游刃有余地进行任务调度的过程。 ####
27 6
|
5天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
26 9
|
4天前
|
缓存 负载均衡 Linux
深入理解Linux内核调度器
本文探讨了Linux操作系统核心组件之一——内核调度器的工作原理和设计哲学。不同于常规的技术文章,本摘要旨在提供一种全新的视角来审视Linux内核的调度机制,通过分析其对系统性能的影响以及在多核处理器环境下的表现,揭示调度器如何平衡公平性和效率。文章进一步讨论了完全公平调度器(CFS)的设计细节,包括它如何处理不同优先级的任务、如何进行负载均衡以及它是如何适应现代多核架构的挑战。此外,本文还简要概述了Linux调度器的未来发展方向,包括对实时任务支持的改进和对异构计算环境的适应性。
22 6
|
5天前
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
21 5
|
5天前
|
算法 Unix Linux
深入理解Linux内核调度器:原理与优化
本文探讨了Linux操作系统的心脏——内核调度器(Scheduler)的工作原理,以及如何通过参数调整和代码优化来提高系统性能。不同于常规摘要仅概述内容,本摘要旨在激发读者对Linux内核调度机制深层次运作的兴趣,并简要介绍文章将覆盖的关键话题,如调度算法、实时性增强及节能策略等。
|
6天前
|
存储 监控 安全
Linux内核调优的艺术:从基础到高级###
本文深入探讨了Linux操作系统的心脏——内核的调优方法。文章首先概述了Linux内核的基本结构与工作原理,随后详细阐述了内核调优的重要性及基本原则。通过具体的参数调整示例(如sysctl、/proc/sys目录中的设置),文章展示了如何根据实际应用场景优化系统性能,包括提升CPU利用率、内存管理效率以及I/O性能等关键方面。最后,介绍了一些高级工具和技术,如perf、eBPF和SystemTap,用于更深层次的性能分析和问题定位。本文旨在为系统管理员和高级用户提供实用的内核调优策略,以最大化Linux系统的效率和稳定性。 ###
|
5天前
|
Java Linux Android开发
深入探索Android系统架构:从Linux内核到应用层
本文将带领读者深入了解Android操作系统的复杂架构,从其基于Linux的内核到丰富多彩的应用层。我们将探讨Android的各个关键组件,包括硬件抽象层(HAL)、运行时环境、以及核心库等,揭示它们如何协同工作以支持广泛的设备和应用。通过本文,您将对Android系统的工作原理有一个全面的认识,理解其如何平衡开放性与安全性,以及如何在多样化的设备上提供一致的用户体验。
|
5天前
|
缓存 运维 网络协议
深入Linux内核架构:操作系统的核心奥秘
深入Linux内核架构:操作系统的核心奥秘
22 2
|
7天前
|
监控 网络协议 算法
Linux内核优化:提升系统性能与稳定性的策略####
本文深入探讨了Linux操作系统内核的优化策略,旨在通过一系列技术手段和最佳实践,显著提升系统的性能、响应速度及稳定性。文章首先概述了Linux内核的核心组件及其在系统中的作用,随后详细阐述了内存管理、进程调度、文件系统优化、网络栈调整及并发控制等关键领域的优化方法。通过实际案例分析,展示了这些优化措施如何有效减少延迟、提高吞吐量,并增强系统的整体健壮性。最终,文章强调了持续监控、定期更新及合理配置对于维持Linux系统长期高效运行的重要性。 ####