LINUX字符设备驱动程序原理总结


LINUX字符设备驱动程序原理总结
2010年07月08日
  一)设备的输入/输出原理
  通常,任何数据都必须通过内核空间才能到达应用程序的缓冲上。例如:对一个设备的读操作会引起数据被至少复制两遍,一遍是将内容复制到内核缓冲中,另一遍是将其再次复制到用户缓冲中。这是为了保证数据的可靠性和安全性所付出的代价。
  但是,当字符设备驱动程序在低速字符设备上读写操作时,它通常直接将数据从用户空间的缓冲中复制到设备上。
  二)I/O和字符设备
  字符设备包括两种类型的设备: 1)低速的字符设备,也就是流设备,一般是终端和连续的端口。 在流设备上不能进行随机访问,也就是只能调用read和write来实现与该设备的通信。而不能使用mmap。由于read和write是同步的,当进程进行write和read调用时,它通常会阻塞,并要一直到操作完成后才能从系统调用里返回。这意味着在存储完成后驱动程序才会允许进程继续执行.本来可以用于运行代码的时间浪费在等待设备驱动程序上.
  用下面的例子说明这一点:
  time dd if=/dev/zero of=/dev/tty bs=1M count=10
  10+0 records in
  10+0 records out
  10485760 bytes (10 MB) copied, 1.99129 s, 5.3 MB/s
  real    0m1.997s
  user    0m0.000s
  sys     0m0.144s
  这里只向终端写了堆空字符串,整个命令用了1.997秒,而实际上花在CPU的时间却只有0.144s,结论是剩下的时间都用在了驱动程序的阻塞上。 2)高速的字符设备 在高速的字符设备上允许随机访问,也就是支持mmap系统调用,这使得应用程序可以查看该设备驱动程序必须提供的所有数据.一般是很大的一个存储区域.
  一个支持mmap系统调用的字符设备驱动程序,是不需要read和write调用的.因为它的读写操作会变得几乎像配给一大块内存一样简单.从而可以减少对系统调用的使用.
  mmap()函数用来将某个文件内容映射到内存中,对该内存区域的访问即是直接对该文件内容读写.调用成功返回映射区的内存起始地址,进程可直接操作起始地址为该值的有效地址,否则返回-1。
  一个简单的mmap示例:
  #include 
  #include 
  #include 
  #include 
  #include 
  #define ERROR(x) do{ perror(x);\
  exit (EXIT_FAILURE); } while(0)
  int
  main (int argc, char *argv[])
  {
  const int nbytes = 4096;
  void *ptr;
  int fd = open("/dev/zero", O_RDWR);  //打开/dev/zero设备
  if (fd == -1) ERROR("open");
  ptr = mmap(0, nbytes, PROT_READ|PROT_WRITE,
  MAP_PRIVATE, fd, 0); //调用mmap函数,将/dev/zero设备节点映射到*ptr中的地址,
  //这个内存区域大小为4096个字节,权限是可读/可写
  if (ptr == MAP_FAILED) ERROR("mmap");
  memset(ptr, 1, nbytes);        //填充*ptr这个内存区域为1
  munmap(ptr, nbytes);         //释放*ptr这个内存映像
  return 0;
  }
  三)块设备,文件系统和I/O
  1)块设备是磁盘及其他使用文件系统的存储设备的基础.
  2)要访问一个设备,可以有两种方法:直接访问或者通过文件系统访问.
  直接访问设备:
  .分区:fdisk
  .格式化:mkfs
  .对设备节点的复制:cp /dev/sda sda.img或者 dd /dev/sda f=sda.img bs=8M
  通过文件系统访问:
  .通过mount命令挂载:mount /dev/sda1 /mnt/disk1之后对/mnt/disk1进行访问
  用一个例子来说明两种访问方式的区别
  备份整个磁盘/dev/sda的内容
  直接访问方式:
  cp /dev/sda sda.img
  通过文件系统访问方式:
  mount /dev/sda1 /mnt/disk1
  tar cvzf /mnt/disk1 sda.tar.gz
  总结两种方式的区别:
  1)直接通过设备节点备份,可以备份磁盘的所有内容,包括启动块,而通过文件系统方式进行备份则不包括启动块等信息。
  2)直接通过设备节点备份是备份了磁盘的所有数据,即磁盘/分区为30GB,备份后的文件也是30GB,而通过对文件系统打包备份,则有多少数据则打包多少数据。
  四)缓冲区缓存和文件系统缓存
  1)缓冲区缓存用来存储块设备可写和可读的块,在操作系统中叫buffer,由块设备驱动程序进行管理.它有以下几个特点:
  .当一个进程要向一个块设备写数据时,首先数据会被复制到缓冲区缓存中的一个块里.块设备驱动程序不是马上被调用,当内核决定应该将数据块写回设备上时,它才会调用驱动程序.
  .内核原则上会尽可能久地将每个读写块的副本保存在缓冲区缓存中..优点一是优化了系统的性能,因为想从磁盘上读数据的进程,只要从高速存储器中读取就可以了..优点二是cache允许内核把临近的数据块连接起来,将它们合并成一个大的写磁盘,这样可以提高磁盘的利用率..优点三cache的应用减少了启动磁盘的次数,因为在被写回磁盘前,一个块已经更新过了,这样内核只需要执行一次写操作而不需要两次.
  .缺点是如果系统崩溃或者掉电,而数据如果还没写到设备上时,数据就会丢失.
  应用缓冲区缓存的例子:
  第一步查看当前系统中memory的buffers的大少,这里是40400
  [root@test1 ~]# free
  total       used       free     shared    buffers     cached
  Mem:        515600     221304     294296          0      40400     143780
  -/+ buffers/cache:      37124     478476
  Swap:      1052248          0    1052248
  第二步从/dev/zero上的一个字符设备复制到了4MB数据到ramdisk设备/dev/ram0上的一个块设备.
  [root@test1 ~]# dd if=/dev/zero f=/dev/ram0 bs=1k count=4096
  第三步查看当前系统中memory的buffers的大小,这里是44428,正好是4MB
  [root@test1 ~]# free
  total       used       free     shared    buffers     cached
  Mem:        515600     225148     290452          0      44428     143780
  -/+ buffers/cache:      36940     478660
  Swap:      1052248          0    1052248
  说明:
  因为上例是与块设备(ramdisk)的互动,所以他就用到了缓冲区缓存.ramdisk设备的一个特性是一旦它分配内存,这块内存空间就不再释放,但不是所有的块设备都会这样做.
  2)文件系统缓存的数据是由文件系统驱动程序管理,在操作系统中叫cache.
  它有以下几个特点:
  .在数据被写到磁盘之前,它会先被复制到文件系统的高速缓存中.
  .内核从磁盘读数据之前,也会试图从文件系统高速缓存中读,并且每次从设备中读到的数据都会复制到文件系统高速缓存中.
  应用操作系统缓存的例子:
  第一步先在ram0设备节点上创建文件系统
  [root@test1 ~]# mkfs -t ext3 /dev/ram0
  mke2fs 1.39 (29-May-2006)
  Filesystem label=
  OS type: Linux
  Block size=4096 (log=2)
  Fragment size=4096 (log=2)
  4096 inodes, 4096 blocks
  204 blocks (4.98%) reserved for the super user
  First data block=0
  1 block group
  32768 blocks per group, 32768 fragments per group
  4096 inodes per group
  Writing inode tables: done                            
  Creating journal (1024 blocks): done
  Writing superblocks and filesystem accounting information: done
  This filesystem will be automatically checked every 25 mounts or
  180 days, whichever comes first.  Use tune2fs -c or -i to override.
  第二步查看cached的大小,为143864
  [root@test1 ~]# free
  total       used       free     shared    buffers     cached
  Mem:        515600     225952     289648          0      45048     143864
  -/+ buffers/cache:      37040     478560
  Swap:      1052248          0    1052248
  第三步挂载/dev/ram0到/mnt
  [root@test1 ~]# mount /dev/ram0 /mnt/
  第四步查看cached的大小,这里没有变化
  [root@test1 ~]# free
  total       used       free     shared    buffers     cached
  Mem:        515600     226108     289492          0      45048     143864
  -/+ buffers/cache:      37196     478404
  Swap:      1052248          0    1052248
  第五步在文件系统/mnt中创建一个2MB的文件.
  [root@test1 ~]# dd if=/dev/zero f=/mnt/zero.dat bs=1k count=2048
  2048+0 records in
  2048+0 records out
  2097152 bytes (2.1 MB) copied, 0.0154719 seconds, 136 MB/s
  第六步最后再查看cached的大小,这里为145912
  [root@test1 ~]# free
  total       used       free     shared    buffers     cached
  Mem:        515600     228092     287508          0      45052     145912
  -/+ buffers/cache:      37128     478472
  Swap:      1052248          0    1052248
  说明:
  新建的文件将一直保存在cache中,直到如下的事件之一发生:
  .新的数据需要占有cache.
  .文件被删除.
  .文件系统没有挂载上.
  .由于进程,内核要刷新空闲内存空间.
  .一个应用程序通过调用sync或者fdatasync直接刷新数据.
  关于文件被删除释放cache中的例子如下:
  第一步,查看cached的大小,这里为145912
  [root@test1 ~]# free
  total       used       free     shared    buffers     cached
  Mem:        515600     228092     287508          0      45052     145912
  -/+ buffers/cache:      37128     478472
  Swap:      1052248          0    1052248
  第二步,删除文件系统中的文件/mnt/zero.dat
  [root@test1 ~]# rm /mnt/zero.dat 
  rm: remove regular file `/mnt/zero.dat'? y
  第三步,查看cached的大小,这里为143908,说明删除文件zero.dat,确实是释放了cache.
  [root@test1 ~]# free
  total       used       free     shared    buffers     cached
  Mem:        515600     228648     286952          0      47108     143908
  -/+ buffers/cache:      37632     477968
  Swap:      1052248          0    1052248
  关于卸载文件系统释放cache的例子如下:
  第一步,在文件系统/mnt中新建4MB的文件
  [root@test1 ~]# dd if=/dev/zero f=/mnt/zero1.dat bs=1k count=4096
  4096+0 records in
  4096+0 records out
  4194304 bytes (4.2 MB) copied, 0.0343595 seconds, 122 MB/s
  第二步,查看当前的cached,这里是147988.
  [root@test1 ~]# free
  total       used       free     shared    buffers     cached
  Mem:        515600     232244     283356          0      47108     147988
  -/+ buffers/cache:      37148     478452
  Swap:      1052248          0    1052248
  第三步,卸载挂载的目录/mnt/
  [root@test1 ~]# umount /mnt/
  第四步,查看当前的cached,这里是143908,说明卸载文件系统,确实是释放了cache.
  [root@test1 ~]# free
  total       used       free     shared    buffers     cached
  Mem:        515600     230632     284968          0      49156     143908
  -/+ buffers/cache:      37568     478032
  Swap:      1052248          0    1052248
  3)缓冲区缓存或文件系统缓存与进程之间关于内存的竞争
  3.1)缓冲区缓存或文件系统cache是空闲的内存空间,因为他们能被刷新为更多的进程腾出空间.
  3.2)一个忙于I/O操作的系统会占用大量的系统内存空间,如缓冲区缓存或者文件系统的cache.
  3.3)如果进程对内存的需要大于当前系统可用的,系统就必须想办法释放空间.为释放空间,内核通过刷新块来收回高速缓冲存储器.
  4)内核收回高速缓冲存储器
  4.1)内核从cache中获得空间将最老的块写回磁盘上.
  4.2)干净的块(clean block)指无需磁盘I/O便可以立刻收回的块.干净的块可以是一个已被磁盘读过但还未被修改的块,也可以是一个被写回磁盘但未被回收的块.
  4.3)脏块(dirty block)指那些已经修改过的块或被创建,且其修改或创建还没有写回磁盘的块.
  五)内核管理文件系统缓存的原理
  内核一般通过后台进程pdflush定期将数据写回到磁盘,它负责确保不让数据过长地占用文件系统缓存(cache).
  例如:在一个空闲的系统中,写了1MB的数据到文件,这个操作会引起内存中出现1MB的dirty cache块.为了防止数据无限期驻留在内存,所以通过pdflush定期限将dirty cache块写回到设备上.在大多数Linux的发行版中这个时间默认为30秒.
  我们再也说下pdflush:
  1)严格说来,pdflush不是一个进程,而是一个内核线程.
  2)一个进程式存在于两个空间.内核线程是一个没有用户空间,完全在内核空间中运行的进程.它是通过由内核定义的函数直接开启,并没有可执行文件.
  3)可以有两种方法识别内核线程:
  方法1)
  查看/proc/PID/maps,在一个普通进程里,它会显示其虚拟内存映射.而在内核线程没有用户空间,它的映射文件总会显示空.
  例如:
  查看init进程,它是一个普通进程.
  [root@test1 ~]# more /proc/1/maps 
  003c2000-003fd000 r-xp 00000000 08:01 3704519    /lib/libsepol.so.1
  003fd000-003fe000 rwxp 0003a000 08:01 3704519    /lib/libsepol.so.1
  003fe000-00408000 rwxp 003fe000 00:00 0 
  004aa000-004bf000 r-xp 00000000 08:01 3704520    /lib/libselinux.so.1
  004bf000-004c1000 rwxp 00015000 08:01 3704520    /lib/libselinux.so.1
  00b51000-00b6a000 r-xp 00000000 08:01 3704501    /lib/ld-2.5.so
  00b6a000-00b6b000 r-xp 00018000 08:01 3704501    /lib/ld-2.5.so
  00b6b000-00b6c000 rwxp 00019000 08:01 3704501    /lib/ld-2.5.so
  00b6e000-00ca5000 r-xp 00000000 08:01 3704502    /lib/libc-2.5.so
  00ca5000-00ca7000 r-xp 00137000 08:01 3704502    /lib/libc-2.5.so
  00ca7000-00ca8000 rwxp 00139000 08:01 3704502    /lib/libc-2.5.so
  00ca8000-00cab000 rwxp 00ca8000 00:00 0 
  00cd6000-00cd8000 r-xp 00000000 08:01 3704503    /lib/libdl-2.5.so
  00cd8000-00cd9000 r-xp 00001000 08:01 3704503    /lib/libdl-2.5.so
  00cd9000-00cda000 rwxp 00002000 08:01 3704503    /lib/libdl-2.5.so
  00d6d000-00d6e000 r-xp 00d6d000 00:00 0          [vdso]
  08048000-08050000 r-xp 00000000 08:01 4915359    /sbin/init
  08050000-08051000 rw-p 00008000 08:01 4915359    /sbin/init
  09fe6000-0a007000 rw-p 09fe6000 00:00 0 
  b7fb5000-b7fb7000 rw-p b7fb5000 00:00 0 
  b7fcb000-b7fcc000 rw-p b7fcb000 00:00 0 
  bf82a000-bf83f000 rw-p bf82a000 00:00 0          [stack]
  结果显示出它所有的映射文件.
  查看pdflush内核线程,首先得到它的PID为136
  ps -ef|grep pdflush
  root       136     7  0 11:34 ?        00:00:00 [pdflush]
  root       137     7  0 11:34 ?        00:00:00 [pdflush]
  查看进程ID为136的maps
  [root@test1 ~]# more /proc/136/maps 
  [root@test1 ~]# 
  显示为空,说明它是一个内核线程
  方法2)
  查看pdflush的父进程
  [root@test1 ~]# ps -ajxf
  PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
  1     7     1     1 ?           -1 S<       0   0:00 [kthread]
  7    10     1     1 ?           -1 S<       0   0:00  \_ [kblockd/0]
  7    11     1     1 ?           -1 S<       0   0:00  \_ [kacpid]
  7    68     1     1 ?           -1 S<       0   0:00  \_ [cqueue/0]
  7    71     1     1 ?           -1 S<       0   0:00  \_ [khubd]
  7    73     1     1 ?           -1 S<       0   0:00  \_ [kseriod]
  7   136     1     1 ?           -1 S        0   0:00  \_ [pdflush]
  7   137     1     1 ?           -1 S        0   0:00  \_ [pdflush]
  7   138     1     1 ?           -1 S<       0   0:00  \_ [kswapd0]
  7   139     1     1 ?           -1 S<       0   0:00  \_ [aio/0]
  7   290     1     1 ?           -1 S<       0   0:00  \_ [kpsmoused]
  7   322     1     1 ?           -1 S<       0   0:00  \_ [scsi_eh_0]
  7   323     1     1 ?           -1 S<       0   0:00  \_ [kjournald]
  7   351     1     1 ?           -1 S<       0   0:00  \_ [kauditd]
  7  1004     1     1 ?           -1 S<       0   0:00  \_ [kmirrord]
  pdflush的父进程是kthread,说明它是由内核线程派生出的一个内核线程.
  最后需要说明的是,在以下两种情况时,内核不会用pdflush将文件系统缓存的数据写回磁盘.
  情况1)因为dirty blocks最有可能是最近使用过的,所以内核不太可能会刷新它们,但当系统正忙于I/O操作时,就有可能刷新它们,在这种情况下,不需要pdflush做刷新工作,而由当前运行的进程来将dirty block写入到磁盘中.
  情况2)应用程序通过fsync,fdataync和sync强迫将特殊文件块提前写回磁盘.
  我们最后需要知道最先收回的块是最老的块,或者是最近最少使用的块.在没有进程进行刷新dirty block和强制的sync时,最后会由pdflush将dirty block写回磁盘.
  为什么都是dirty block写回磁盘呢,因为改过的块不写入磁盘,会因为掉电,当机等情况丢失数据,而clean block不存在这样的问题.

猜你喜欢

转载自vu060vu.iteye.com/blog/1362423
今日推荐