全部博文(142)
分类: LINUX
2011-10-04 11:25:11
一)设备的输入/输出原理
通常,任何数据都必须通过内核空间才能到达应用程序的缓冲上。例如:对一个设备的读操作会引起数据被至少复制两遍,一遍是将内容复制到内核缓冲中,另一遍是将其再次复制到用户缓冲中。这是为了保证数据的可靠性和安全性所付出的代价。
但是,当字符设备驱动程序在低速字符设备上读写操作时,它通常直接将数据从用户空间的缓冲中复制到设备上。
二)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不存在这样的问题.