linux 驱动 并发、(非)阻塞、时钟中断

来源:互联网 发布:贵金属看盘软件 编辑:程序博客网 时间:2024/05/16 01:36

竞态

并发情况下防止竞争的措施:

(1)中断屏蔽

(2)原子操作

(3)自旋锁

(4)读写自旋锁(防写不防读)

(5)顺序锁(seqlock)

(6)RCU(Read-Copy-Update)

(7)信号量(信号量其实和自旋锁是一样的,就是有一点不同:当获取不到信号量时,进程不会原地打转而是进入休眠等待状态)

(8)完成量(completion),它用于一个执行单元等待另一个执行单元执行完某事

(9)读写信号量

(10)互斥体(mutex)

经验:定义带有设备并发控制方案的结构体(诸如信号量,自旋锁等)


(1)在Linux设备驱动中,可以使用等待队列(wait queue)来实现阻塞进程的唤醒.等待队列能够用于实现内核中的异步事件通知机制。

(2)使用非阻塞I/O的应用程序通常会使用select()和poll()系统调用查询是否可对设备进行无阻塞的访问,这两个系统调用最终又会引发设备驱动中的poll()函数被执行,设备驱动中的poll()函数原型:

unsigned int (*poll)(struct file *filp, struct poll_table *wait);

void poll_wait(struct file *filp, wait_queue_head_t *queue, poll_table *wait);

常量说明POLLIN普通或优先级带数据可读POLLRDNORM普通数据可读POLLRDBAND优先级带数据可读POLLPRI高优先级数据可读POLLOUT普通数据可写POLLWRNORM普通数据可写POLLWRBAND优先级带数据可写POLLERR发生错误POLLHUP发生挂起POLLNVAL描述字不是一个打开的文件


驱动程序中的异步通知:

123


时钟与中断:

1)设备申请中断

2)释放中断

3)使能和屏蔽中断

Linux系统中中断是分为顶半部和底半部的,低半部通过tasklet,工作队列,软中断实现。

向量中断就是入口地址不同,进不同的地址做不同的事。那非向量中断则是进同一地址,至于区分就放在了进去后用条件判断。

tasklet使用模版:

void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(XXX_tasklet, xxx_do_tasklet, 0);
void xxx_do_tasklet(unsigned long)   //中断处理底半部
{
    .....
}
irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs)  //中断处理顶半部
{
  ...
  tasklet_schedule(&xxx_tasklet);
}
int __init xxx_init(void)   //设备驱动模块加载函数
{
  ..
  result= request_irq(xxx_irq, xxx_interrupt, SA_INTERRUPT, "XXX",NULL);  //申请中断
  ...
}
void __exit xxx_exit(void)   //设备驱动卸载模块
{
  ..
  free_irq(xxx_irq, xxx_interrupt);   //释放中断
  ..
}

工作队列模版:

struct work_struct xxx_wq;
void xxx_do_work(unsigned long);
void xxx_do_work(unsigned long)   //中断处理底半部
{
    .....
}
irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs)  //中断处理顶半部
{
  ...
  schedule_work(&xxx_wq);
}
int xxx_init(void)   //设备驱动模块加载函数
{
  ..
  result= request_irq(xxx_irq, xxx_interrupt, SA_INTERRUPT, "XXX",NULL);  //申请中断
  ...
  INIT_WORK(&xxx_wq, (void (*)(void *))xxx_do_work, NULL);
    ...
}
void __exit xxx_exit(void)   //设备驱动卸载模块
{
  ..
  free_irq(xxx_irq, xxx_interrupt);   //释放中断
  ..
}

中断共享模版:

irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs)  //中断处理顶半部
{
  ...
  int status = read_int_status();  //获取终端源
  if(!is_myint(dev_id, status))  //判断是否是本设备的中断
  {
     return  IRQ_NONE://立即返回
  }
  ..
  return IRQ_HANDLED;
}
int __init xxx_init(void)   //设备驱动模块加载函数
{
  ..
  result= request_irq(xxx_irq, xxx_interrupt, SA_SHIRQ, "XXX",xxx_dev);  //申请共享中断
  ...
}
void __exit xxx_exit(void)   //设备驱动卸载模块
{
  ..
  free_irq(xxx_irq, xxx_interrupt);   //释放中断
  ..
}

内核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序执行update_process_timers函数,该函数调用run_local_timers函数,这个函数处理TIMER_SOFTIRQ软中断,运行当前处理上到期的所有定时器。

定时器的模版:

struct xxx_dev  /*second设备结构体*/
{
  struct cdev cdev; /*cdev结构体*/
  ...
  struct timer_list xxx_timer; /*设备要使用的定时器*/
};
int xxx_func1(...)  //xxx驱动中某函数
{
  struct xxx_dev *dev = filp->private_data;
    ...
  /*初始化定时器*/
  init_timer(&dev->xxx_timer);
  dev->xxx_timer.function = &xxx_do_handle;
  dev->xxx_timer.data = (unsigned long)dev;
  dev->xxx_timer.expires = jiffies + delay;
 
  add_timer(&dev->xxx_timer); /*添加(注册)定时器*/
  ...
  return 0;
}
int xxx_func2(...)   //驱动中某函数
{
  ...
  del_timer(&second_devp->s_timer);
  ...
}
static void xxx_do_timer(unsigned long arg)  //定时器处理函数
{
  struct xxx_device *dev = (struct xxx_device *)(arg);
    ...
    //调度定时器再执行
  dev->xxx_timer.expires = jiffies + delay;
  add_timer(&dev->xxx_timer);
}

内核延迟函数:

1)短延迟

2)长延迟

3)睡着延迟

IO内存、IO端口读写,申请,映射。

http://www.cnblogs.com/hanyan225/archive/2010/10/26/1861431.html

一般情况下,用户空间是不可能也不应该直接访问设备的,但是设备驱动程序可实现mmap()函数,这个函数可使得用户空间能直接访问设备的物理地址。实际上,mmap()实现了这样的一个映射过程,它将用户空间的一段内存与设备内存关联,当用户访问用户空间的这段地址范围时,实际上会转化为对设备的访问。

 mmp()必须以PAGE_SIZE为单位进行映射,实际上,内存只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行页对齐,强行以PAGE_SIZE的倍数大小进行映射。驱动中mmp()函数原型如下:int (*mmp)(struct file *, struct vm_area_struct *);它实现的机制是建立页表,并填充VMA结构体中 vm_operations_struct指针,vm_area_struct用于描述一个虚拟内存区域:

Linux内核中,关于虚存管理的最基本的管理单元应该是struct vm_area_struct了,它描述的是一段连续的、具有相同访问属性的虚存空间,该虚存空间的大小为物理内存页面的整数倍

struct vm_area_struct {    struct mm_struct * vm_mm; //所处的地址空间    unsigned long vm_start;//开始虚拟地址    unsigned long vm_end;//结束虚拟地址    struct vm_area_struct *vm_next;    pgprot_t vm_page_prot;  //访问权限    unsigned long vm_flags; //标志     ...    struct vm_operations_struct * vm_ops; //操作VMA的函数集指针    unsigned long vm_pgoff; //偏移(页帧号)    struct file * vm_file;    void * vm_private_data;    ...};
vm_area_struct结构所描述的虚存空间以vm_start、vm_end成员表示,它们分别保存了该虚存空间的首地址和末地址后第一个字节的地址,以字节为单位,所以虚存空间范围可以用[vm_start, vm_end)表示。

假如该vm_area_struct描述的是一个文件映射的虚存空间,成员vm_file便指向被映射的文件的file结构,vm_pgoff是该虚存空间起始地址在vm_file文件里面的文件偏移,单位为物理页面。

进程建立vm_area_struct结构后,只是说明进程可以访问这个虚存空间,但有可能还没有分配相应的物理页面并建立好页面映射。在这种情况下,若是进程执行中有指令需要访问该虚存空间中的内存,便会产生一次缺页异常。这时候,就需要通过vm_area_struct结构里面的vm_ops->nopage所指向的函数来将产生缺页异常的地址对应的文件数据读取出来。

进程虚拟地址示意图


mmap系统调用(功能)

void * mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset)

参数:

addr

指定映射的起始地址(通常不指定)通常为NULL,由系统指定

length

映射到内存的文件长度

prot

映射区的保护方式:

PROT_EXEC:映射区可被执行

PROT_READ:映射区可被读取

PROTWRITE:映射区可被写入

flags

映射区的特性:

MAP_SHARED:写入映射区的内容最后要写入文件

MAP_PRIVATE:最后不会写入文件

fd

open返回的文件描述符,代表要映射的文件

offset

以文件开始处的偏移量,必须是分布大小的整数倍,通常为0,表示从文件头开始映射

返回

会返回起始地址,本来mmap是指向内存地址的指针

内存映射函数mmap负责把文件内容映射到进程的虚拟内存空间,通过对这段内存的读取和修改,来实现对文件的读取和修改,而不需要再调用readwrite等操作。直接用指针操作文件的内容。

图中左边的是进程的虚拟空间,右边的是文件


mmap设备操作

映射一个设备是指 把用户空间的一段地址关联到设备内存上

当程序读写这段用户空间的地址时,它实际上是在访问设备。

步骤

1)找到用户空间的地址(内核自动帮你做好)

2)找到设备的物理地址(查看芯片手册)

3)关联 (通过页式管理)

 mmap设备方法所需要做的就是建立虚拟地址到物理地址的页表

int (*mmap)(struct file * , struct vm_area-strcut *)

                               ↓

                           内核帮我找的

mmap如何完成页表的建立?

方法有二:

1)使用remap_pfn_range一次建立所有页表

2)使用nopage VMA方法每次建立一个页表

int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn,unsigned long size, pgprot_t prot)

vma

虚拟内存区域指针

virt_addr

虚拟地址的起始值

pfn

要映射的物理地址所在的物理页帧号(物理地址的序列号),可将物理地址>>PGE_SHIFT得到,即右移12位,相当于除以4k2^12

prot

VMA的保护属性



// 内核模块加载函数int __init kmalloc_map_init(void){    ../申请设备号,添加cedv结构体  buffer = kmalloc(BUF_SIZE, GFP_KERNEL); //申请buffer  for(page = virt_to_page(buffer); page< virt_to_page(buffer+BUF_SIZE); page++)  {     mem_map_reserve(page);  //置业为保留  }}//mmap()函数static int kmalloc_map_mmap(struct file *filp, struct vm_area_struct *vma){    unsigned long page, pos;    unsigned long start = (unsigned long)vma->start;    unsigned long size = (unsigned long)(vma->end - vma->start);    printk(KERN_INFO, "mmaptest_mmap called\n");    if(size > BUF_SIZE)  //用户要映射的区域太大        return - EINVAL;    pos = (unsigned long)buffer;    while(size > 0)   //映射buffer中的所有页    {        page = virt_to_phys((void *)pos);        if(remap_page_range(start, page, PAGE_SIZE, PAGE_SHARRED))            return -EAGAIN;        start += PAGE_SIZE;        pos +=PAGE_SIZE;        size -= PAGE_SIZE;    }    return 0;}

另外通常,IO内存被映射时需要是nocache的,这个时候应该对vma->vm_page_prot设置nocache标志。如下:

static int xxx_nocache_mmap(struct file *filp, struct vm_area_struct *vma){  vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);   //赋nocache标志  vma->vm_pgoff = ((u32)map_start >> PAGE_SHIFT);  if(rempa_pfn_range(vma, vma->vm_start, vma->vm_pgoff, vma->vm_end - vm_start, vma->vm_page_prot));     return - EAGGIN;  return 0;}

这段代码中的pgprot_noncached()是一个宏,它实际上禁止了相关页的cache和写缓冲(write buffer),另外一个稍微少的一些限制的宏是:

#define pgprot_writecombine(prot)  __pgprot(pgprot_val (prot) & –L_PTE_CACHEABLE);    它则没有禁止写缓冲

而除了rempa_pfn_range()外,在驱动程序中实现VMA的nopage()函数通常可以为设备提供更加灵活的内存映射途径。当发生缺页时,nopage()会被内核自动调用,。这是因为,当发生缺页异常时,系统会经过如下处理过程:

1)找到缺页的虚拟地址所在的VMA             2)如果必要,分配中间页目录表和页表              

3)如果页表项对应的物理页表不存在,则调用这个VMA的nopage()方法,它返回物理页面的页描述符。

4)将物理页面的地址填充到页表中。

实现nopage后,用户空间可以通过mremap()系统调用重新绑定映射区所绑定的地址,下面给出一个在设备驱动中使用nopage()的典型范例:

static int xxx_mmap(struct file *filp, struct vm_area_struct *vma);{     unsigned long offset = vma->vm_pgoff << PAGE_OFFSET;     if(offset >= _ _pa(high_memory) || (filp->flags &O_SYNC))             vma->vm_flags |=VM_IO;     vma->vm_ops = &xxx_nopage_vm_ops;     xxx_vma_open(vma);     return 0;}struct page *xxx_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type){   struct page *pageptr;   unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;   unsigned long physaddr = address - vma->vm_start + offset;   //物理内存   unsigned long pageframe = physaddr >> PAGE_SHIFT;  //页帧号   if(!pfn_valid(pageframe))   //页帧号有效      return NOPAGE_SIGBUS;   pageptr = pfn_to_page(pageframe);    //页帧号->页描述符   get_page(pageptr);   //获得页,增加页的使用计数   if(type)      *type = VM_FAULT_MINOR;   return pageptr;    //返回页描述符
}

上述函数对常规内存进行映射,返回一个页描述符,可用于扩大或缩小映射的内存区域,由此可见,nopage()和remap_pfn_range()一个较大的区别在于remap_pfn

_range()一般用于设备内存映射,而nopage()还可以用于RAM映射。


原创粉丝点击