﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-深海拾贝-文章分类-Linux驱动</title><link>http://www.cppblog.com/coloerful/category/10645.html</link><description /><language>zh-cn</language><lastBuildDate>Fri, 22 May 2009 09:11:23 GMT</lastBuildDate><pubDate>Fri, 22 May 2009 09:11:23 GMT</pubDate><ttl>60</ttl><item><title>Linux设备驱动编程之内存与I/O操作</title><link>http://www.cppblog.com/coloerful/articles/85421.html</link><dc:creator>小猪</dc:creator><author>小猪</author><pubDate>Fri, 22 May 2009 08:26:00 GMT</pubDate><guid>http://www.cppblog.com/coloerful/articles/85421.html</guid><wfw:comment>http://www.cppblog.com/coloerful/comments/85421.html</wfw:comment><comments>http://www.cppblog.com/coloerful/articles/85421.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/coloerful/comments/commentRss/85421.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/coloerful/services/trackbacks/85421.html</trackback:ping><description><![CDATA[对于提供了MMU（存储管理器，辅助操作系统进行内存管理，提供虚实地址转换等硬件支持）的处理器而言，Linux提供了复杂的存储管理系统，使得进程所能访问的内存达到4GB。
<p>　　进程的4GB内存空间被人为的分为两个部分--用户空间与内核空间。用户空间地址分布从0到3GB(PAGE_OFFSET，在0x86中它等于0xC0000000)，3GB到4GB为内核空间，如下图：</p>
<p>
<table align=center>
    <tbody>
        <tr>
            <td>
            <div align=center><img src="http://www.unixreference.net/upimg/allimg/20061031/0223520.jpg" border=1></div>
            </td>
        </tr>
    </tbody>
</table>
</p>
<p>　　内核空间中，从3G到vmalloc_start这段地址是物理内存映射区域（该区域中包含了内核镜像、物理页框表mem_map等等），比如我们使用的VMware虚拟系统内存是160M，那么3G～3G+160M这片内存就应该映射物理内存。在物理内存映射区之后，就是vmalloc区域。对于 160M的系统而言，vmalloc_start位置应在3G+160M附近（在物理内存映射区与vmalloc_start期间还存在一个8M的gap 来防止跃界），vmalloc_end的位置接近4G(最后位置系统会保留一片128k大小的区域用于专用页面映射)，如下图：</p>
<p align=center><img src="http://www.unixreference.net/upimg/allimg/20061031/0223521.jpg" border=1></p>
<p>　　kmalloc和get_free_page申请的内存位于物理内存映射区域，而且在物理上也是连续的，它们与真实的物理地址只有一个固定的偏移，因此存在较简单的转换关系，virt_to_phys()可以实现内核虚拟地址转化为物理地址：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)<br>extern inline unsigned long virt_to_phys(volatile void * address)<br>{<br>　return __pa(address);<br>}</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　上面转换过程是将虚拟地址减去3G（PAGE_OFFSET=0XC000000）。</p>
<p>　　与之对应的函数为phys_to_virt()，将内核物理地址转化为虚拟地址：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))<br>extern inline void * phys_to_virt(unsigned long address)<br>{<br>　return __va(address);<br>}</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　virt_to_phys()和phys_to_virt()都定义在include\asm-i386\io.h中。</p>
<p><br>　　而vmalloc申请的内存则位于vmalloc_start～vmalloc_end之间，与物理地址没有简单的转换关系，虽然在逻辑上它们也是连续的，但是在物理上它们不要求连续。</p>
<p>　　我们用下面的程序来演示kmalloc、get_free_page和vmalloc的区别：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>#include &lt;linux/module.h&gt;<br>#include &lt;linux/slab.h&gt;<br>#include &lt;linux/vmalloc.h&gt;<br>MODULE_LICENSE("GPL"); <br>unsigned char *pagemem;<br>unsigned char *kmallocmem;<br>unsigned char *vmallocmem;<br><br>int __init mem_module_init(void)<br>{<br>　//最好每次内存申请都检查申请是否成功<br>　//下面这段仅仅作为演示的代码没有检查<br>　pagemem = (unsigned char*)get_free_page(0);<br>　printk("&lt;1&gt;pagemem addr=%x", pagemem);<br><br>　kmallocmem = (unsigned char*)kmalloc(100, 0);<br>　printk("&lt;1&gt;kmallocmem addr=%x", kmallocmem);<br><br>　vmallocmem = (unsigned char*)vmalloc(1000000);<br>　printk("&lt;1&gt;vmallocmem addr=%x", vmallocmem);<br><br>　return 0;<br>}<br><br>void __exit mem_module_exit(void)<br>{<br>　free_page(pagemem);<br>　kfree(kmallocmem);<br>　vfree(vmallocmem);<br>}<br><br>module_init(mem_module_init);<br>module_exit(mem_module_exit);</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　我们的系统上有160MB的内存空间，运行一次上述程序，发现pagemem的地址在0xc7997000（约3G+121M）、kmallocmem 地址在0xc9bc1380（约3G+155M）、vmallocmem的地址在0xcabeb000（约3G+171M）处，符合前文所述的内存布局。</p>
<p>　　接下来，我们讨论Linux设备驱动究竟怎样访问外设的I/O端口（寄存器）。</p>
<p>　　几乎每一种外设都是通过读写设备上的寄存器来进行的，通常包括控制寄存器、状态寄存器和数据寄存器三大类，外设的寄存器通常被连续地编址。根据CPU体系结构的不同，CPU对IO端口的编址方式有两种：</p>
<p>　　（1）I/O映射方式（I/O-mapped）</p>
<p>　　典型地，如X86处理器为外设专门实现了一个单独的地址空间，称为"I/O地址空间"或者"I/O端口空间"，CPU通过专门的I/O指令（如X86的IN和OUT指令）来访问这一空间中的地址单元。 </p>
<p>　　（2）内存映射方式（Memory-mapped）</p>
<p>　　RISC指令系统的CPU（如ARM、PowerPC等）通常只实现一个物理地址空间，外设I/O端口成为内存的一部分。此时，CPU可以象访问一个内存单元那样访问外设I/O端口，而不需要设立专门的外设I/O指令。</p>
<p>　　但是，这两者在硬件实现上的差异对于软件来说是完全透明的，驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是"I/O内存"资源。</p>
<p>　　一般来说，在系统运行时，外设的I/O内存资源的物理地址是已知的，由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围，驱动程序并不能直接通过物理地址访问I/O内存资源，而必须将它们映射到核心虚地址空间内（通过页表），然后才能根据映射所得到的核心虚地址范围，通过访内指令访问这些I/O内存资源。Linux在io.h头文件中声明了函数ioremap（），用来将I/O内存资源的物理地址映射到核心虚地址空间（3GB－4GB）中，原型如下：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　iounmap函数用于取消ioremap（）所做的映射，原型如下：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>void iounmap(void * addr);</td>
        </tr>
    </tbody>
</table>
</p>
这两个函数都是实现在mm/ioremap.c文件中。
<p>　　在将I/O内存资源的物理地址映射成核心虚地址后，理论上讲我们就可以象读写RAM那样直接读写I/O内存资源了。为了保证驱动程序的跨平台的可移植性，我们应该使用Linux中特定的函数来访问I/O内存资源，而不应该通过指向核心虚地址的指针来访问。如在x86平台上，读写I/O的函数如下所示：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>#define readb(addr) (*(volatile unsigned char *) __io_virt(addr))<br>#define readw(addr) (*(volatile unsigned short *) __io_virt(addr))<br>#define readl(addr) (*(volatile unsigned int *) __io_virt(addr))<br><br>#define writeb(b,addr) (*(volatile unsigned char *) __io_virt(addr) = (b))<br>#define writew(b,addr) (*(volatile unsigned short *) __io_virt(addr) = (b))<br>#define writel(b,addr) (*(volatile unsigned int *) __io_virt(addr) = (b))<br><br>#define memset_io(a,b,c) memset(__io_virt(a),(b),(c))<br>#define memcpy_fromio(a,b,c) memcpy((a),__io_virt(b),(c))<br>#define memcpy_toio(a,b,c) memcpy(__io_virt(a),(b),(c))</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　最后，我们要特别强调驱动程序中mmap函数的实现方法。用mmap映射一个设备，意味着使用户空间的一段地址关联到设备内存上，这使得只要程序在分配的地址范围内进行读取或者写入，实际上就是对设备的访问。</p>
<p>　　笔者在Linux源代码中进行包含"ioremap"文本的搜索，发现真正出现的ioremap的地方相当少。所以笔者追根索源地寻找I/O操作的物理地址转换到虚拟地址的真实所在，发现Linux有替代ioremap的语句，但是这个转换过程却是不可或缺的。</p>
<p>　　譬如我们再次摘取S3C2410这个ARM芯片RTC（实时钟）驱动中的一小段：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>static void get_rtc_time(int alm, struct rtc_time *rtc_tm)<br>{<br>　spin_lock_irq(&amp;rtc_lock);<br>　if (alm == 1) {<br>　　rtc_tm-&gt;tm_year = (unsigned char)ALMYEAR &amp; Msk_RTCYEAR;<br>　　rtc_tm-&gt;tm_mon = (unsigned char)ALMMON &amp; Msk_RTCMON;<br>　　rtc_tm-&gt;tm_mday = (unsigned char)ALMDAY &amp; Msk_RTCDAY;<br>　　rtc_tm-&gt;tm_hour = (unsigned char)ALMHOUR &amp; Msk_RTCHOUR;<br>　　rtc_tm-&gt;tm_min = (unsigned char)ALMMIN &amp; Msk_RTCMIN;<br>　　rtc_tm-&gt;tm_sec = (unsigned char)ALMSEC &amp; Msk_RTCSEC;<br>　}<br>　else {<br>　　read_rtc_bcd_time:<br>　　rtc_tm-&gt;tm_year = (unsigned char)BCDYEAR &amp; Msk_RTCYEAR;<br>　　rtc_tm-&gt;tm_mon = (unsigned char)BCDMON &amp; Msk_RTCMON;<br>　　rtc_tm-&gt;tm_mday = (unsigned char)BCDDAY &amp; Msk_RTCDAY;<br>　　rtc_tm-&gt;tm_hour = (unsigned char)BCDHOUR &amp; Msk_RTCHOUR;<br>　　rtc_tm-&gt;tm_min = (unsigned char)BCDMIN &amp; Msk_RTCMIN;<br>　　rtc_tm-&gt;tm_sec = (unsigned char)BCDSEC &amp; Msk_RTCSEC;<br><br>　　if (rtc_tm-&gt;tm_sec == 0) {<br>　　　/* Re-read all BCD registers in case of BCDSEC is 0.<br>　　　See RTC section at the manual for more info. */<br>　　　goto read_rtc_bcd_time;<br>　　}<br>　}<br>　spin_unlock_irq(&amp;rtc_lock);<br><br>　BCD_TO_BIN(rtc_tm-&gt;tm_year);<br>　BCD_TO_BIN(rtc_tm-&gt;tm_mon);<br>　BCD_TO_BIN(rtc_tm-&gt;tm_mday);<br>　BCD_TO_BIN(rtc_tm-&gt;tm_hour);<br>　BCD_TO_BIN(rtc_tm-&gt;tm_min);<br>　BCD_TO_BIN(rtc_tm-&gt;tm_sec);<br><br>　/* The epoch of tm_year is 1900 */<br>　rtc_tm-&gt;tm_year += RTC_LEAP_YEAR - 1900;<br><br>　/* tm_mon starts at 0, but rtc month starts at 1 */<br>　rtc_tm-&gt;tm_mon--;<br>}</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　I/O操作似乎就是对ALMYEAR、ALMMON、ALMDAY定义的寄存器进行操作，那这些宏究竟定义为什么呢？</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>#define ALMDAY bRTC(0x60)<br>#define ALMMON bRTC(0x64)<br>#define ALMYEAR bRTC(0x68)</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　其中借助了宏bRTC，这个宏定义为：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>#define bRTC(Nb) __REG(0x57000000 + (Nb))</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　其中又借助了宏__REG，而__REG又定义为：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td># define __REG(x) io_p2v(x)</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　最后的io_p2v才是真正"玩"虚拟地址和物理地址转换的地方： </p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>#define io_p2v(x) ((x) | 0xa0000000)</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　与__REG对应的有个__PREG：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td># define __PREG(x) io_v2p(x)</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　与io_p2v对应的有个io_v2p：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>#define io_v2p(x) ((x) &amp; ~0xa0000000)</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　可见有没有出现ioremap是次要的，关键问题是有无虚拟地址和物理地址的转换！</p>
<p>　　下面的程序在启动的时候保留一段内存，然后使用ioremap将它映射到内核虚拟空间，同时又用remap_page_range映射到用户虚拟空间，这样一来，内核和用户都能访问。如果在内核虚拟地址将这段内存初始化串"abcd"，那么在用户虚拟地址能够读出来：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>
            <p>/************mmap_ioremap.c**************/<br>#include &lt;linux/module.h&gt;<br>#include &lt;linux/kernel.h&gt;<br>#include &lt;linux/errno.h&gt;<br>#include &lt;linux/mm.h&gt;<br>#include &lt;linux/wrapper.h&gt; /* for mem_map_(un)reserve */<br>#include &lt;asm/io.h&gt; /* for virt_to_phys */<br>#include &lt;linux/slab.h&gt; /* for kmalloc and kfree */<br><br>MODULE_PARM(mem_start, "i");<br>MODULE_PARM(mem_size, "i");<br><br>static int mem_start = 101, mem_size = 10;<br>static char *reserve_virt_addr;<br>static int major;<br><br>int mmapdrv_open(struct inode *inode, struct file *file);<br>int mmapdrv_release(struct inode *inode, struct file *file);<br>int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma);<br><br>static struct file_operations mmapdrv_fops =<br>{<br>　owner: THIS_MODULE, mmap: mmapdrv_mmap, open: mmapdrv_open, release:<br>　mmapdrv_release,<br>};<br><br>int init_module(void)<br>{<br>　if ((major = register_chrdev(0, "mmapdrv", &amp;mmapdrv_fops)) &lt; 0)<br>　{<br>　　printk("mmapdrv: unable to register character device\n");<br>　　return ( - EIO);<br>　}<br>　printk("mmap device major = %d\n", major);<br><br>　printk("high memory physical address 0x%ldM\n", virt_to_phys(high_memory) /<br>1024 / 1024);</p>
            </td>
        </tr>
    </tbody>
</table>
</p>
reserve_virt_addr = ioremap(mem_start *1024 * 1024, mem_size *1024 * 1024);<br>　printk("reserve_virt_addr = 0x%lx\n", (unsigned long)reserve_virt_addr);<br>　if (reserve_virt_addr)<br>　{<br>　　int i;<br>　　for (i = 0; i &lt; mem_size *1024 * 1024; i += 4)<br>　　{<br>　　　reserve_virt_addr[i] = 'a';<br>　　　reserve_virt_addr[i + 1] = 'b';<br>　　　reserve_virt_addr[i + 2] = 'c';<br>　　　reserve_virt_addr[i + 3] = 'd';<br>　　}<br>　}<br>　else<br>　{<br>　　unregister_chrdev(major, "mmapdrv");<br>　　return - ENODEV;<br>　}<br>　return 0;<br>}<br><br>/* remove the module */<br>void cleanup_module(void)<br>{<br>　if (reserve_virt_addr)<br>　　iounmap(reserve_virt_addr);<br><br>　unregister_chrdev(major, "mmapdrv");<br>　return ;<br>}<br><br>int mmapdrv_open(struct inode *inode, struct file *file)<br>{<br>　MOD_INC_USE_COUNT;<br>　return (0);<br>}<br><br>int mmapdrv_release(struct inode *inode, struct file *file)<br>{<br>　MOD_DEC_USE_COUNT;<br>　return (0);<br>}<br><br>int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma)<br>{<br>　unsigned long offset = vma-&gt;vm_pgoff &lt;&lt; PAGE_SHIFT;<br>　unsigned long size = vma-&gt;vm_end - vma-&gt;vm_start;<br><br>　if (size &gt; mem_size *1024 * 1024)<br>　{<br>　　printk("size too big\n");<br>　　return ( - ENXIO);<br>　}<br><br>　offset = offset + mem_start * 1024 * 1024;<br><br>　/* we do not want to have this area swapped out, lock it */<br>　vma-&gt;vm_flags |= VM_LOCKED;<br>　if (remap_page_range(vma, vma-&gt;vm_start, offset, size, PAGE_SHARED))<br>　{<br>　　printk("remap page range failed\n");<br>　　return - ENXIO;<br>　}<br>　return (0);<br>}
<p>　　remap_page_range函数的功能是构造用于映射一段物理地址的新页表，实现了内核空间与用户空间的映射，其原型如下： </p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>int remap_page_range(vma_area_struct *vma, unsigned long from, unsigned long to, unsigned long size, pgprot_tprot); </td>
        </tr>
    </tbody>
</table>
</p>
<p>　　使用mmap最典型的例子是显示卡的驱动，将显存空间直接从内核映射到用户空间将可提供显存的读写效率。</p>
<p>--<br>原文链接: <a href="http://dev.yesky.com/412/2639912.shtml" target=_blank><a onmousedown=javascript:event.cancelBubble=true; href="http://dev.yesky.com/412/2639912.shtml" target=_blank><u><font color=#000000>http://dev.yesky.com/412/2639912.shtml</font></u></a></p>
<img src ="http://www.cppblog.com/coloerful/aggbug/85421.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/coloerful/" target="_blank">小猪</a> 2009-05-22 16:26 <a href="http://www.cppblog.com/coloerful/articles/85421.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>深入浅出Linux设备驱动之字符设备驱动程序</title><link>http://www.cppblog.com/coloerful/articles/85419.html</link><dc:creator>小猪</dc:creator><author>小猪</author><pubDate>Fri, 22 May 2009 08:25:00 GMT</pubDate><guid>http://www.cppblog.com/coloerful/articles/85419.html</guid><wfw:comment>http://www.cppblog.com/coloerful/comments/85419.html</wfw:comment><comments>http://www.cppblog.com/coloerful/articles/85419.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/coloerful/comments/commentRss/85419.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/coloerful/services/trackbacks/85419.html</trackback:ping><description><![CDATA[<div style="FONT-SIZE: 14px; LINE-HEIGHT: 1.5em">Linux下的设备驱动程序被组织为一组完成不同任务的函数的集合，通过这些函数使得Windows的设备操作犹如文件一般。在应用程序看来，硬件设备只是一个设备文件，应用程序可以象操作普通文件一样对硬件设备进行操作，如open ()、close ()、read ()、write () 等。
<p>　　Linux主要将设备分为二类：字符设备和块设备。字符设备是指设备发送和接收数据以字符的形式进行；而块设备则以整个数据缓冲区的形式进行。字符设备的驱动相对比较简单。</p>
<p>　　下面我们来假设一个非常简单的虚拟字符设备：这个设备中只有一个4个字节的全局变量int global_var，而这个设备的名字叫做"gobalvar"。对"gobalvar"设备的读写等操作即是对其中全局变量global_var的操作。</p>
<p>　　驱动程序是内核的一部分，因此我们需要给其添加模块初始化函数，该函数用来完成对所控设备的初始化工作，并调用register_chrdev() 函数注册字符设备：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>static int __init gobalvar_init(void)<br>{<br>　if (register_chrdev(MAJOR_NUM, " gobalvar ", &amp;gobalvar_fops))<br>　{<br>　　//&#8230;注册失败<br>　}<br>　else<br>　{<br>　　//&#8230;注册成功<br>　}<br>}</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　其中，register_chrdev函数中的参数MAJOR_NUM为主设备号,"gobalvar"为设备名，gobalvar_fops为包含基本函数入口点的结构体，类型为file_operations。当gobalvar模块被加载时，gobalvar_init被执行，它将调用内核函数 register_chrdev，把驱动程序的基本入口点指针存放在内核的字符设备地址表中，在用户进程对该设备执行系统调用时提供入口地址。</p>
<p>　　与模块初始化函数对应的就是模块卸载函数，需要调用register_chrdev()的"反函数" unregister_chrdev()：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>static void __exit gobalvar_exit(void)<br>{<br>　if (unregister_chrdev(MAJOR_NUM, " gobalvar "))<br>　{<br>　　//&#8230;卸载失败<br>　}<br>　else<br>　{<br>　　//&#8230;卸载成功<br>　}<br>}</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　随着内核不断增加新的功能，file_operations结构体已逐渐变得越来越大，但是大多数的驱动程序只是利用了其中的一部分。对于字符设备来说，要提供的主要入口有：open ()、release ()、read ()、write ()、ioctl ()、llseek()、poll()等。</p>
<p>　　open()函数　对设备特殊文件进行open()系统调用时，将调用驱动程序的open () 函数：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>int (*open)(struct inode * ,struct file *);</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　其中参数inode为设备特殊文件的inode (索引结点) 结构的指针，参数file是指向这一设备的文件结构的指针。open()的主要任务是确定硬件处在就绪状态、验证次设备号的合法性(次设备号可以用 MINOR(inode-&gt; i - rdev) 取得)、控制使用设备的进程数、根据执行情况返回状态码(0表示成功，负数表示存在错误) 等；</p>
<p>release()函数　当最后一个打开设备的用户进程执行close ()系统调用时，内核将调用驱动程序的release () 函数：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>void (*release) (struct inode * ,struct file *) ;</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　release 函数的主要任务是清理未结束的输入/输出操作、释放资源、用户自定义排他标志的复位等。</p>
<p>　　read()函数　当对设备特殊文件进行read() 系统调用时，将调用驱动程序read() 函数：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>ssize_t (*read) (struct file *, char *, size_t, loff_t *);</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　用来从设备中读取数据。当该函数指针被赋为NULL 值时，将导致read 系统调用出错并返回-EINVAL（"Invalid argument，非法参数"）。函数返回非负值表示成功读取的字节数（返回值为"signed size"数据类型，通常就是目标平台上的固有整数类型）。</p>
<p>　　globalvar_read函数中内核空间与用户空间的内存交互需要借助第2节所介绍的函数：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)<br>{<br>　&#8230;<br>　copy_to_user(buf, &amp;global_var, sizeof(int));<br>　&#8230;<br>}</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　write( ) 函数　当设备特殊文件进行write () 系统调用时，将调用驱动程序的write () 函数：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>ssize_t (*write) (struct file *, const char *, size_t, loff_t *);</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　向设备发送数据。如果没有这个函数，write 系统调用会向调用程序返回一个-EINVAL。如果返回值非负，则表示成功写入的字节数。</p>
<p>　　globalvar_write函数中内核空间与用户空间的内存交互需要借助第2节所介绍的函数：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t　*off)<br>{<br>&#8230;<br>copy_from_user(&amp;global_var, buf, sizeof(int));<br>&#8230;<br>}</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　ioctl() 函数　该函数是特殊的控制函数，可以通过它向设备传递控制信息或从设备取得状态信息，函数原型为： </p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>int (*ioctl) (struct inode * ,struct file * ,unsigned int ,unsigned long);</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　unsigned int参数为设备驱动程序要执行的命令的代码，由用户自定义，unsigned long参数为相应的命令提供参数，类型可以是整型、指针等。如果设备不提供ioctl 入口点，则对于任何内核未预先定义的请求，ioctl 系统调用将返回错误（-ENOTTY，"No such ioctl fordevice，该设备无此ioctl 命令"）。如果该设备方法返回一个非负值，那么该值会被返回给调用程序以表示调用成功。</p>
<p>　　llseek()函数 该函数用来修改文件的当前读写位置，并将新位置作为（正的）返回值返回，原型为： </p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>loff_t (*llseek) (struct file *, loff_t, int);</td>
        </tr>
    </tbody>
</table>
</p>
poll()函数 poll 方法是poll 和select 这两个系统调用的后端实现，用来查询设备是否可读或可写，或是否处于某种特殊状态，原型为：<br>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>unsigned int (*poll) (struct file *, struct poll_table_struct *);</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　我们将在"设备的阻塞与非阻塞操作"一节对该函数进行更深入的介绍。</p>
<p>　　设备"gobalvar"的驱动程序的这些函数应分别命名为gobalvar_open、gobalvar_ release、gobalvar_read、gobalvar_write、gobalvar_ioctl，因此设备"gobalvar"的基本入口点结构变量gobalvar_fops 赋值如下：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>struct file_operations gobalvar_fops = {<br>　read: gobalvar_read,<br>　write: gobalvar_write,<br>};</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　上述代码中对gobalvar_fops的初始化方法并不是标准C所支持的，属于GNU扩展语法。</p>
<p>　　完整的globalvar.c文件源代码如下：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>
            <p>#include &lt;linux/module.h&gt;<br>#include &lt;linux/init.h&gt;<br>#include &lt;linux/fs.h&gt;<br>#include &lt;asm/uaccess.h&gt; <br>MODULE_LICENSE("GPL");<br><br>#define MAJOR_NUM 254 //主设备号<br><br>static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);<br>static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);<br><br>//初始化字符设备驱动的file_operations结构体<br>struct file_operations globalvar_fops =<br>{<br>　read: globalvar_read, write: globalvar_write,<br>};<br>static int global_var = 0; //"globalvar"设备的全局变量<br><br>static int __init globalvar_init(void)<br>{<br>　int ret;<br><br>　//注册设备驱动<br>　ret = register_chrdev(MAJOR_NUM, "globalvar", &amp;globalvar_fops);<br>　if (ret)<br>　{<br>　　printk("globalvar register failure");<br>　}<br>　else<br>　{<br>　　printk("globalvar register success");<br>　}<br>　return ret;<br>}</p>
            </td>
        </tr>
    </tbody>
</table>
</p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>
            <p>static void __exit globalvar_exit(void)<br>{<br>　int ret;<br><br>　//注销设备驱动<br>　ret = unregister_chrdev(MAJOR_NUM, "globalvar");<br>　if (ret)<br>　{<br>　　printk("globalvar unregister failure");<br>　}<br>　else<br>　{<br>　　printk("globalvar unregister success");<br>　}<br>}<br><br>static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)<br>{<br>　//将global_var从内核空间复制到用户空间<br>　if (copy_to_user(buf, &amp;global_var, sizeof(int)))<br>　{<br>　　return - EFAULT;<br>　} <br>　return sizeof(int);<br>}<br><br>static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t　*off)<br>{<br>　//将用户空间的数据复制到内核空间的global_var<br>　if (copy_from_user(&amp;global_var, buf, sizeof(int)))<br>　{<br>　　return - EFAULT;<br>　} <br>　return sizeof(int);<br>}<br><br>module_init(globalvar_init);<br>module_exit(globalvar_exit);</p>
            </td>
        </tr>
    </tbody>
</table>
<p>&nbsp;　　运行：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>gcc -D__KERNEL__ -DMODULE -DLINUX -I /usr/local/src/linux2.4/include -c -o globalvar.o globalvar.c</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　编译代码，运行：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>inmod globalvar.o</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　加载globalvar模块，再运行：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>cat /proc/devices</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　发现其中多出了"254 globalvar"一行，如下图：</p>
<p>
<table align=center>
    <tbody>
        <tr>
            <td>
            <div align=center><img src="http://www.unixreference.net/upimg/allimg/20061031/0335400.jpg" border=1></div>
            </td>
        </tr>
    </tbody>
</table>
</p>
<p>　　接着我们可以运行：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>mknod /dev/globalvar c 254 0</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　创建设备节点，用户进程通过/dev/globalvar这个路径就可以访问到这个全局变量虚拟设备了。我们写一个用户态的程序globalvartest.c来验证上述设备：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>#include &lt;sys/types.h&gt;<br>#include &lt;sys/stat.h&gt;<br>#include &lt;stdio.h&gt;<br>#include &lt;fcntl.h&gt;<br>main()<br>{<br>　int fd, num;<br>　//打开"/dev/globalvar"<br>　fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);<br>　if (fd != -1 )<br>　{<br>　　//初次读globalvar<br>　　read(fd, &amp;num, sizeof(int));<br>　　printf("The globalvar is %d\n", num);<br><br>　　//写globalvar<br>　　printf("Please input the num written to globalvar\n");<br>　　scanf("%d", &amp;num);<br>　　write(fd, &amp;num, sizeof(int));<br><br>　　//再次读globalvar<br>　　read(fd, &amp;num, sizeof(int));<br>　　printf("The globalvar is %d\n", num);<br><br>　　//关闭"/dev/globalvar"<br>　　close(fd);<br>　}<br>　else<br>　{<br>　　printf("Device open failure\n");<br>　}<br>}</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　编译上述文件：</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>gcc -o globalvartest.o globalvartest.c</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　运行</p>
<p>
<table align=center border=0>
    <tbody>
        <tr>
            <td>./globalvartest.o</td>
        </tr>
    </tbody>
</table>
</p>
<p>　　可以发现"globalvar"设备可以正确的读写。</p>
<p>--<br>原文链接: <a href="http://dev.yesky.com/186/2623186_1.shtml" target=_blank><a onmousedown=javascript:event.cancelBubble=true; href="http://dev.yesky.com/186/2623186_1.shtml" target=_blank><u><font color=#000000>http://dev.yesky.com/186/2623186_1.shtml</font></u></a></a></p>
</div>
<img src ="http://www.cppblog.com/coloerful/aggbug/85419.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/coloerful/" target="_blank">小猪</a> 2009-05-22 16:25 <a href="http://www.cppblog.com/coloerful/articles/85419.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>如何编写Linux设备驱动程序</title><link>http://www.cppblog.com/coloerful/articles/85418.html</link><dc:creator>小猪</dc:creator><author>小猪</author><pubDate>Fri, 22 May 2009 08:24:00 GMT</pubDate><guid>http://www.cppblog.com/coloerful/articles/85418.html</guid><wfw:comment>http://www.cppblog.com/coloerful/comments/85418.html</wfw:comment><comments>http://www.cppblog.com/coloerful/articles/85418.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/coloerful/comments/commentRss/85418.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/coloerful/services/trackbacks/85418.html</trackback:ping><description><![CDATA[<strong>序言</strong>
<p>　　Linux是Unix操作系统的一种变种，在Linux下编写驱动程序的原理和思想完全类似于其他的Unix系统，但它与dos或window环境下的驱动程序有很大的区别。在Linux环境下设计驱动程序，思想简洁，操作方便，功能也很强大，但是支持函数少，只能依赖kernel中的函数，有些常用的操作要自己来编写，而且调试也不方便。本人这几周来为实验室自行研制的一块多媒体卡编制了驱动程序，获得了一些经验，愿与Linux fans共享，有不当之处，请予指正。</p>
<p>　　以下的一些文字主要来源于khg，johnsonm的Write linux device driver，Brennan's Guide to Inline Assembly，The Linux A-Z，还有清华BBS上的有关device driver的一些资料. 这些资料有的已经过时，有的还有一些错误，我依据自己的试验结果进行了修正. </p>
<p>　　<strong>一、Linux device driver 的概念</strong> </p>
<p>　　系统调用是操作系统内核和应用程序之间的接口，设备驱动程序是操作系统内核和机器硬件之间的接口.设备驱动程序为应用程序屏蔽了硬件的细节，这样在应用程序看来，硬件设备只是一个设备文件， 应用程序可以象操作普通文件一样对硬件设备进行操作.设备驱动程序是内核的一部分，它完成以下的功能: </p>
<p>　　1.对设备初始化和释放. <br>　　2.把数据从内核传送到硬件和从硬件读取数据. <br>　　3.读取应用程序传送给设备文件的数据和回送应用程序请求的数据. <br>　　4.检测和处理设备出现的错误. </p>
<p>　　在Linux操作系统下有两类主要的设备文件类型，一种是字符设备，另一种是块设备.字符设备和块设备的主要区别是:在对字符设备发出读/写请求时，实际的硬件I/O一般就紧接着发生了，块设备则不然，它利用一块系统内存作缓冲区，当用户进程对设备请求能满足用户的要求，就返回请求的数据，如果不能，就调用请求函数来进行实际的I/O操作.块设备是主要针对磁盘等慢速设备设计的，以免耗费过多的CPU时间来等待. </p>
<p>　　已经提到，用户进程是通过设备文件来与实际的硬件打交道.每个设备文件都都有其文件属性(c/b)，表示是字符设备还蔤强樯璞?另外每个文件都有两个设备号，第一个是主设备号，标识驱动程序，第二个是从设备号，标识使用同一个设备驱动程序的不同的硬件设备，比如有两个软盘，就可以用从设备号来区分他们.设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致，否则用户进程将无法访问到驱动程序. </p>
<p>　　最后必须提到的是，在用户进程调用驱动程序时，系统进入核心态，这时不再是抢先式调度.也就是说，系统必须在你的驱动程序的子函数返回后才能进行其他的工作.如果你的驱动程序陷入死循环，不幸的是你只有重新启动机器了，然后就是漫长的fsck.//hehe </p>
<p>　　读/写时，它首先察看缓冲区的内容，如果缓冲区的数据<font color=#c0c0c0>(UNIX Reference注: 此处不完整，由于找不到原始的文字，所以无法进行修正，请谅解。)</font></p>
<p>　　<strong>二、实例剖析 </strong></p>
<p>　　我们来写一个最简单的字符设备驱动程序。虽然它什么也不做，但是通过它可以了解Linux的设备驱动程序的工作原理.把下面的C代码输入机器，你就会获得一个真正的设备驱动程序.不过我的kernel是2.0.34，在低版本的kernel上可能会出现问题，我还没测试过.//xixi </p>
<span>
<p>　　#define __NO_VERSION__ <br>　　#include &lt;linux/modules.h&gt; <br>　　#include &lt;linux/version.h&gt; </p>
<p>　　char kernel_version [] = UTS_RELEASE; </p>
</span>
<p>　　这一段定义了一些版本信息，虽然用处不是很大，但也必不可少.Johnsonm说所有的驱动程序的开头都要包含&lt;linux/config.h&gt;，但我看倒是未必. </p>
<p>　　由于用户进程是通过设备文件同硬件打交道，对设备文件的操作方式不外乎就是一些系统调用，如 open，read，write，close....， 注意，不是fopen， fread，但是如何把系统调用和驱动程序关联起来呢?这需要了解一个非常关键的数据结构: </p>
<table cellSpacing=1 cellPadding=4 align=center border=0>
    <tbody>
        <tr>
            <td bgColor=#ffffff><span>
            <p>struct file_operations <br>{ <br>&nbsp; int (*seek) (struct inode * ，struct file *， off_t ，int); <br>&nbsp; int (*read) (struct inode * ，struct file *， char ，int); <br>&nbsp; int (*write) (struct inode * ，struct file *， off_t ，int); <br>&nbsp; int (*readdir) (struct inode * ，struct file *， struct dirent * ，int); <br>&nbsp; int (*select) (struct inode * ，struct file *， int ，select_table *); <br>&nbsp; int (*ioctl) (struct inode * ，struct file *， unsined int ，unsigned long); <br>&nbsp; int (*mmap) (struct inode * ，struct file *， struct vm_area_struct *); <br>&nbsp; int (*open) (struct inode * ，struct file *); <br>&nbsp; int (*release) (struct inode * ，struct file *); <br>&nbsp; int (*fsync) (struct inode * ，struct file *); <br>&nbsp; int (*fasync) (struct inode * ，struct file *，int); <br>&nbsp; int (*check_media_change) (struct inode * ，struct file *); <br>&nbsp; int (*revalidate) (dev_t dev); <br>} <br></p>
            </span></td>
        </tr>
    </tbody>
</table>
<p>　　这个结构的每一个成员的名字都对应着一个系统调用.用户进程利用系统调用在对设备文件进行诸如read/write操作时，系统调用通过设备文件的主设备号找到相应的设备驱动程序，然后读取这个数据结构相应的函数指针，接着把控制权交给该函数.这是linux的设备驱动程序工作的基本原理.既然是这样，则编写设备驱动程序的主要工作就是编写子函数，并填充file_operations的各个域. </p>
<p>下面就开始写子程序.
<table cellSpacing=1 cellPadding=4 align=center border=0>
    <tbody>
        <tr>
            <td bgColor=#ffffff><span>
            <p>#include &lt;linux/types.h&gt; <br>#include &lt;linux/fs.h&gt; <br>#include &lt;linux/mm.h&gt; <br>#include &lt;linux/errno.h&gt; <br>#include &lt;asm/segment.h&gt; </p>
            <p>unsigned int test_major = 0; </p>
            <p>static int read_test(struct inode *node, struct file *file, char *buf, int count) <br>{ <br>&nbsp; int left; </p>
            <p>&nbsp; if (verify_area(VERIFY_WRITE, buf, count) == -EFAULT ) <br>&nbsp;&nbsp;&nbsp; return -EFAULT; </p>
            <p>&nbsp; for(left = count ; left &gt; 0 ; left--) <br>&nbsp; { <br>&nbsp;&nbsp;&nbsp; __put_user(1, buf, 1); <br>&nbsp;&nbsp;&nbsp; buf++; <br>&nbsp; } </p>
            <p>&nbsp; return count; <br>}</p>
            </span></td>
        </tr>
    </tbody>
</table>
</p>
<p>　　这个函数是为read调用准备的.当调用read时，read_test()被调用，它把用户的缓冲区全部写1.buf 是read调用的一个参数.它是用户进程空间的一个地址.但是在read_test被调用时，系统进入核心态.所以不能使用buf这个地址，必须用__put_user()，这是kernel提供的一个函数，用于向用户传送数据.另外还有很多类似功能的函数.请参考.在向用户空间拷贝数据之前，必须验证buf是否可用。<br>　　这就用到函数verify_area. </p>
<p>
<table cellSpacing=1 cellPadding=4 align=center border=0>
    <tbody>
        <tr>
            <td bgColor=#ffffff><span>
            <p>static int write_tibet(struct inode *inode, struct file *file, const char *buf, int count) <br>{ <br>&nbsp; return count; <br>} </p>
            <p>static int open_tibet(struct inode *inode, struct file *file ) <br>{ <br>&nbsp; MOD_INC_USE_COUNT; <br>&nbsp; return 0; <br>} </p>
            <p>static void release_tibet(struct inode *inode, struct file *file ) <br>{ <br>&nbsp; MOD_DEC_USE_COUNT; <br>} <br></p>
            </span></td>
        </tr>
    </tbody>
</table>
</p>
<p>　　这几个函数都是空操作.实际调用发生时什么也不做，他们仅仅为下面的结构提供函数指针。 </p>
<p>
<table cellSpacing=1 cellPadding=4 align=center border=0>
    <tbody>
        <tr>
            <td bgColor=#ffffff><span>struct file_operations test_fops = <br>{ <br>&nbsp; NULL， <br>&nbsp; read_test， <br>&nbsp; write_test， <br>&nbsp; NULL， /* test_readdir */ <br>&nbsp; NULL， <br>&nbsp; NULL， /* test_ioctl */ <br>&nbsp; NULL， /* test_mmap */ <br>&nbsp; open_test， <br>&nbsp; release_test， NULL， /* test_fsync */ <br>&nbsp; NULL， /* test_fasync */ <br>&nbsp; /* nothing more， fill with NULLs */ <br>}; </span></td>
        </tr>
    </tbody>
</table>
</p>
<p>　　设备驱动程序的主体可以说是写好了。现在要把驱动程序嵌入内核。驱动程序可以按照两种方式编译。一种是编译进kernel，另一种是编译成模块(modules)，如果编译进内核的话，会增加内核的大小，还要改动内核的源文件，而且不能动态的卸载，不利于调试，所以推荐使用模块方式。 </p>
<p>
<table cellSpacing=1 cellPadding=4 align=center border=0>
    <tbody>
        <tr>
            <td bgColor=#ffffff><span>
            <p>int init_module(void) <br>{ <br>&nbsp; int result; </p>
            <p>&nbsp; result = register_chrdev(0, "test", &amp;test_fops); </p>
            <p>&nbsp; if (result &lt; 0) <br>&nbsp; { <br>&nbsp;&nbsp;&nbsp; printk(KERN_INFO "test: can't get major number\n"); <br>&nbsp;&nbsp;&nbsp; return result; <br>&nbsp; } </p>
            <p>&nbsp; if (test_major == 0) test_major = result; /* dynamic */ <br>&nbsp;&nbsp;&nbsp; return 0; <br>} </p>
            </span></td>
        </tr>
    </tbody>
</table>
</p>
在用insmod命令将编译好的模块调入内存时，init_module 函数被调用。在这里，init_module只做了一件事，就是向系统的字符设备表登记了一个字符设备。register_chrdev需要三个参数，参数一是希望获得的设备号，如果是零的话，系统将选择一个没有被占用的设备号返回。参数二是设备文件名，参数三用来登记驱动程序实际执行操作的函数的指针。
<p>　　如果登记成功，返回设备的主设备号，不成功，返回一个负值。 </p>
<table cellSpacing=1 cellPadding=4 align=center border=0>
    <tbody>
        <tr>
            <td bgColor=#ffffff><span>void cleanup_module(void) <br>{ <br>&nbsp; unregister_chrdev(test_major， "test"); <br>} </span></td>
        </tr>
    </tbody>
</table>
<p>　　在用rmmod卸载模块时，cleanup_module函数被调用，它释放字符设备test在系统字符设备表中占有的表项。 </p>
<p>　　一个极其简单的字符设备可以说写好了，文件名就叫test.c吧。 </p>
<p>　　下面编译 </p>
<p>　　<span>$ gcc -O2 -DMODULE -D__KERNEL__ -c test.c </span></p>
<p>　　得到文件test.o就是一个设备驱动程序。 </p>
<p>　　如果设备驱动程序有多个文件，把每个文件按上面的命令行编译，然后 </p>
<p>　　<span>ld -r file1.o file2.o -o modulename. </span>　</p>
<p>　　驱动程序已经编译好了，现在把它安装到系统中去。 </p>
<p>　　<span>$ insmod -f test.o</span> </p>
<p>　　如果安装成功，在/proc/devices文件中就可以看到设备test，并可以看到它的主设备号。 <br><br>　　要卸载的话，运行 </p>
<p>　　<span>$ rmmod test</span> </p>
<p>　　下一步要创建设备文件。 </p>
<p>　　<span>mknod /dev/test c major minor</span> </p>
<p>　　c 是指字符设备，major是主设备号，就是在/proc/devices里看到的。 </p>
<p>　　用shell命令 </p>
<p>　　<span>$ cat /proc/devices | awk "\\$2==\"test\" {print \\$1}"</span> </p>
<p>　　就可以获得主设备号，可以把上面的命令行加入你的shell&nbsp; script&nbsp; 中去。 </p>
<p>　　minor是从设备号，设置成0就可以了。 </p>
<p>　　我们现在可以通过设备文件来访问我们的驱动程序。写一个小小的测试程序。 </p>
<table cellSpacing=1 cellPadding=4 align=center border=0>
    <tbody>
        <tr>
            <td bgColor=#ffffff><span>
            <p>#include &lt;stdio.h&gt; <br>#include &lt;sys/types.h&gt; <br>#include &lt;sys/stat.h&gt; <br>#include &lt;fcntl.h&gt; </p>
            <p>main() <br>{ <br>&nbsp; int testdev; <br>&nbsp; int i; <br>&nbsp; char buf[10]; </p>
            <p>&nbsp; testdev = open("/dev/test", O_RDWR); </p>
            <p>&nbsp; if ( testdev == -1 ) <br>&nbsp; { <br>&nbsp;&nbsp;&nbsp; printf("Cann't open file \n"); <br>&nbsp;&nbsp;&nbsp; exit(0); <br>&nbsp; } </p>
            <p>&nbsp; read(testdev, buf, 10); </p>
            <p>&nbsp; for (i = 0; i &lt; 10;i++) <br>&nbsp;&nbsp;&nbsp; printf("%d\n", buf[i]); </p>
            <p>&nbsp; close(testdev); <br>}</p>
            </span></td>
        </tr>
    </tbody>
</table>
<p>　　编译运行，看看是不是打印出全1 ？ </p>
<p>　　以上只是一个简单的演示。真正实用的驱动程序要复杂的多，要处理如中断，DMA，I/O port等问题。这些才是真正的难点。请看下节，实际情况的处理。 </p>
<p><strong>三、设备驱动程序中的一些具体问题</strong></p>
<p>　　1. I/O Port. </p>
<p>　　和硬件打交道离不开I/O Port，老的ISA设备经常是占用实际的I/O端口，在linux下，操作系统没有对I/O口屏蔽，也就是说，任何驱动程序都可对任意的I/O口操作，这样就很容易引起混乱。每个驱动程序应该自己避免误用端口。 </p>
<p>　　有两个重要的kernel函数可以保证驱动程序做到这一点。 </p>
<p>　　1）<span>check_region(int io_port， int off_set)</span> </p>
<p>　　这个函数察看系统的I/O表，看是否有别的驱动程序占用某一段I/O口。 </p>
<p>　　参数1：io端口的基地址， </p>
<p>　　参数2：io端口占用的范围。 </p>
<p>　　返回值：0 没有占用， 非0，已经被占用。 </p>
<p>　　2）<span>request_region(int io_port， int off_set，char *devname)</span> </p>
<p>　　如果这段I/O端口没有被占用，在我们的驱动程序中就可以使用它。在使用之前，必须向系统登记，以防止被其他程序占用。登记后，在/proc/ioports文件中可以看到你登记的io口。 </p>
<p>　　参数1：io端口的基地址。 </p>
<p>　　参数2：io端口占用的范围。 </p>
<p>　　参数3：使用这段io地址的设备名。 </p>
<p>　　在对I/O口登记后，就可以放心地用inb()， outb()之类的函来访问了。 </p>
<p>　　在一些pci设备中，I/O端口被映射到一段内存中去，要访问这些端口就相当于访问一段内存。经常性的，我们要获得一块内存的物理地址。在dos环境下，（之所以不说是dos操作系统是因为我认为DOS根本就不是一个操作系统，它实在是太简单，太不安全了）只要用段：偏移就可以了。在window95中，95ddk提供了一个vmm 调用 _MapLinearToPhys，用以把线性地址转化为物理地址。但在Linux中是怎样做的呢？ </p>
<p>　　2.内存操作 </p>
<p>　　在设备驱动程序中动态开辟内存，不是用malloc，而是kmalloc，或者用get_free_pages直接申请页。释放内存用的是kfree，或free_pages. 请注意，kmalloc等函数返回的是物理地址！而malloc等返回的是线性地址！关于kmalloc返回的是物理地址这一点本人有点不太明白：既然从线性地址到物理地址的转换是由386cpu硬件完成的，那样汇编指令的操作数应该是线性地址，驱动程序同样也不能直接使用物理地址而是线性地址。但是事实上kmalloc返回的确实是物理地址，而且也可以直接通过它访问实际的RAM，我想这样可以由两种解释，一种是在核心态禁止分页，但是这好像不太现实；另一种是linux的页目录和页表项设计得正好使得物理地址等同于线性地址。我的想法不知对不对，还请高手指教。 </p>
<p>　　言归正传，要注意kmalloc最大只能开辟128k-16，16个字节是被页描述符结构占用了。kmalloc用法参见khg. </p>
<p>　　内存映射的I/O口，寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问，要通过kernel函数vremap获得重新映射以后的地址。 </p>
<p>　　另外，很多硬件需要一块比较大的连续内存用作DMA传送。这块内存需要一直驻留在内存，不能被交换到文件中去。但是kmalloc最多只能开辟128k的内存。 </p>
<p>　　这可以通过牺牲一些系统内存的方法来解决。 </p>
<p>　　具体做法是：比如说你的机器由32M的内存，在lilo.conf的启动参数中加上mem=30M，这样linux就认为你的机器只有30M的内存，剩下的2M内存在vremap之后就可以为DMA所用了。 </p>
<p>　　请记住，用vremap映射后的内存，不用时应用unremap释放，否则会浪费页表。 </p>
<p>　　3.中断处理 </p>
<p>　　同处理I/O端口一样，要使用一个中断，必须先向系统登记。 </p>
<p>
<table cellSpacing=1 cellPadding=4 align=center border=0>
    <tbody>
        <tr>
            <td bgColor=#ffffff>
            <p><span>int request_irq(unsigned int irq , void(*handle)(int, void *, struct pt_regs *), unsigned int long flags, const char *device); </span></p>
            <p>irq: 是要申请的中断。 </p>
            <p>handle：中断处理函数指针。 </p>
            <p>flags：SA_INTERRUPT 请求一个快速中断, 0 正常中断。 </p>
            <p>device：设备名。</p>
            </td>
        </tr>
    </tbody>
</table>
</p>
<p>　　如果登记成功，返回0，这时在/proc/interrupts文件中可以看你请求的中断。 </p>
<p>　　4.一些常见的问题。 </p>
<p>　　对硬件操作，有时时序很重要。但是如果用C语言写一些低级的硬件操作的话，gcc往往会对你的程序进行优化，这样时序就错掉了。如果用汇编写呢，gcc同样会对汇编代码进行优化，除非你用volatile关键字修饰。最保险的办法是禁止优化。这当然只能对一部分你自己编写的代码。如果对所有的代码都不优化，你会发现驱动程序根本无法装载。这是因为在编译驱动程序时要用到gcc的一些扩展特性，而这些扩展特性必须在加了优化选项之后才能体现出来。</p>
<p>--<br>原文链接: <a href="http://www.chinaitpower.com/A/2004-05-21/159420.html" target=_blank><a onmousedown=javascript:event.cancelBubble=true; href="http://www.chinaitpower.com/A/2004-05-21/159420.html" target=_blank><u><font color=#000000>http://www.chinaitpower.com/A/2004-05-21/159420.html</font></u></a></p>
<img src ="http://www.cppblog.com/coloerful/aggbug/85418.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/coloerful/" target="_blank">小猪</a> 2009-05-22 16:24 <a href="http://www.cppblog.com/coloerful/articles/85418.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>