linux下如何做到从键盘快捷键使用大全输入字符不显示问题

个人出版图书:《玩转Python网络爬虫》、《玩转Django2.0》


帧缓冲(framebuffer)是 Linux 为显示设备提供的一个接口把显存抽象后的一种设备,他允许上层应用程序在图形模式下直接对显示缓冲区進行读写操作这种操作是抽象的,统一的用户不必关心物理显存的位置、换页机制等等具体细节。这些都是由Framebuffer 设备驱动来完成的帧緩冲驱动的应用广泛,在 linux 的桌面系统中Xwindow 服务器就是利用帧缓冲进行窗口的绘制。尤其是通过帧缓冲可显示汉字点阵成为 Linux汉化的唯一可荇方案。

帧缓冲设备对应的设备文件为/dev/fb*如果系统有多个显示卡,Linux 下还可支持多个帧缓冲设备最多可达 32个,分别为/dev/fb0 到/dev/fb31而/dev/fb 则为当前缺省嘚帧缓冲设备,通常指向/dev/fb0当然在嵌入式系统中支持一个显示设备就够了。帧缓冲设备为标准linux字符设备设备主设备号为29,次设备号则从0箌31分别对应/dev/fb0-/dev/fb31。

通过/dev/fb应用程序的操作主要有这几种:

2.映射(map)操作:由于 Linux工作在保护模式,每个应用程序都有自己的虚拟地址空间茬应用程序中是不能直接访问物理缓冲区地址的。为此Linux 在文件操作file_operations 结构中提供了 mmap函数,可将文件的内容映射到用户空间对于帧缓冲设備,则可通过映射操作可将屏幕缓冲区的物理地址映射到用户空间的一段虚拟地址中,之后用户就可以通过读写这段虚拟地址访问屏幕緩冲区在屏幕上绘图了。

3.I/O控制:对于帧缓冲设备对设备文件的 ioctl操作可读取/设置显示设备及屏幕的参数,如分辨率显示颜色数,屏幕大小等等ioctl 的操作是由底层的驱动程序来完成的。

在应用程序中操作/dev/fb的一般步骤如下:

2.用 ioctrl 操作取得当前显示屏幕的参数,如屏幕分辨率每个像素点的比特数。根据屏幕参数可计算屏幕缓冲区的大小

3.将屏幕缓冲区映射到用户空间(mmap)。

4.映射后就可以直接读写屏幕缓冲区进行绘图和图片显示了。

下载百度知道APP抢鲜体验

使用百度知道APP,立即抢鲜体验你的手机镜头里或许有别人想知道的答案。

本文自顶向下一步步探索linux字符设備设备的读写是怎么完成的通常我们在Linux应用程序中用open、read、write对各种类型的文件进行操作。我们可以从键盘输入然后命令行窗口会显示你嘚输入,有输出的话则命令行窗口会显示输出为什么所有的设备在Linux中都被看成是一个个文件,可以通过统一的read、write直接进行读写文件句柄与终端设备有什么关联?为什么Linux允许多个控制终端登录tty又是什么东西?读写时将发生哪些硬件中断驱动程序是怎么回事?微型计算機原理与接口技术中的串口在Linux是怎么用的对于这些疑问,本文将通过Linux 0.11版本的源码找到解答!

接着遍历全局文件结构file_tableNR_FILE=64include/linux/fs.hp39545行),檢查占用次数是否为零(因为file_table已经在内核的数据区中不用再申请空间,这里检查的是count是否为零而不是是否为NULLtask_struct是指针数组检查该项昰否为NULL确定是否被占用),找到后用当前句柄对其进行关联引用计数加一(最后是)。然后使用open_namei找到该打开文件的inode对于linux字符设备设備文件,如果是串口设备且是在没有控制终端的会话领导进程打开则设置当前进程的控制终端为串口次设备号(主串口或者辅串口),串口的前台进程组为当前的进程的进程组号如果要打开控制台(键盘和显示屏),但当前进程没有控制终端(current->tty=-1)则不能打开控制终端设备攵件。另外该函数还能处理块设备文件最后关联文件指针和inode节点,返回文件句柄(其实就是当前进程文件指针数组的下标)

从这里可鉯看出,一个进程最多可以打开20个文件而一个系统最多可以打开64个文件,每个进程的每一个文件指针都要消耗全局进程表的一项一个設备文件节点的核心之处在于inode->i_zone[0],也就是linux字符设备设备号内核通过设备号定位具体的设备,对该设备进行读写

首先利用fd获得当前进程的file指针,然后获得对应的inode文件类型有linux字符设备设备文件、块设备文件、目录文件、普通文件和匿名管道,这里根据inode->i_mode进行确定然后调用具體的文件操作函数。所以辨别文件类型是通过inode->i_mode而却像一个大文件一样读写(拥有文件读取位置)。这也就是为什么所有的文件都可以用readwrite来读写且只需传递fd即可。将linux字符设备设备号(保存在linux字符设备设备节点inode->i_zone[0])传递给rw_char函数而一个文件指针的作用仅是保存文件的当前位置(file->f_pos)。值得注意的是文件的当前位置对linux字符设备设备来说没有作用

 上述函数以主设备号为数组下标,将次设备号作为参数调用对应的设備函数。注意一种设备只有一个主设备号而同一种设备数量可以有多个,对应的便是多个次设备号上述串口主设备号是4,调用的函数昰rw_ttyx控制终端的主设备号是5,调用的函数是rw_tty

从上面可以看出不管是串口还是控制台终端,实际调用的函数是tty_readtty_write传递的都是次设备号,苴文件位置pos不起作用只不过控制台终端要求进程必须有控制终端,传进来的minor次设备号被忽略使用当前进程的控制终端代替(current->tty)。注意rw_char操作的是设备号而不是inode。

2.3 上层接口结构图


从上面可知传递过来的次设备号被用来索引tty_table这个数组,进而获得对应的tty设备的内核数据结构对于tty_read,从tty->secondary获取数据写到用户态的buf中,当tty->secondary队列为空或者没有EOF和换行符且linux字符设备太少时,当前进程都会进入可中断的休眠状态;对于tty_write从用户态的buf写数据到tty->write_q,并调用tty->write(tty)表示将数据立即显示或者提醒串口输出数据。

        这里主要存放linux字符设备设备的标志且每个标志占用一个仳特,这些标志将影响对读入数据的解释尤其要注意的是本地模式标志,设置ICANON可以启用规范模式

pgrp是一个前台进程组号,而write是一个函数指针tty_write函数每次将用户态的数据写往write_q,并调用tty->write(tty)对于控制台,这个函数是con_write取走write_q中的数据到显存里,在显示屏显示对于串口,这个函数昰rs_write提醒串口有数据可以写了,等待写到数据口发送出去这里有点类似面向对象中的多态。

read_q是由中断程序操作的串口或者键盘有数据箌达时,就会有产生中断然后保存到read_q中。read_q中的数据是原始数据中断时还会调用copy_to_cooked,将其做进一步的处理并将处理过的数据保存在secondary辅助隊列中。从上面tty_read中可以看到tty_read读取的实际是secondary队列中的数据也就是经过处理的数据。另外从上面tty_table数组的初始化可以看出,串口read_qwrite_qdata都是数據口的地址而secondarydatasecondary中数据的行数。

 尤其注意proc_list对于读进程,当secondary没有数据时将当前进程设置为可中断休眠,当数据到达时(由copy_to_cooked唤醒)会將进程设置为可运行状态对于写进程,当write_q满时将当前进程设置为可中断休眠,当write_q全部写完时(由串口中write_char子例程唤醒)会将进程设置为鈳运行状态

con_write这个函数从write_q中获取一个linux字符设备,如果ASCII位于32–126之间也就是可以显示的linux字符设备,直接显示linux字符设备即可(可能要换行因為屏幕一般是25行,80列同时还要注意设置linux字符设备的属性,也就是前景和背景的颜色等)对于ASCII0– 31, 127其实是控制linux字符设备,必须进行特殊處理如\n= 10表示换行调到下一行的相同位置,\r = 13表示回车回到当前行的开头,BEL= 7表示扬声器发声8表示退格符删除左边一个linux字符设备,\t= 9向下个8的整數倍的位置移动光标控制序列(CSI)以ESCASCII=27)开头,如ESC[7m]是将linux字符设备显示为白底黑字(反显)代码中的case1-case4都是在处理以ESC开头的控制序列。例孓:write(fd,“hello\tworld!\n”,

这里的origin是显示屏显示区域的起始地址而且是个虚拟地址。而x,y是坐标0<=x<=80,0<=y<25pos是当前光标的虚拟地址不过它是针对0xB8000而言的。对于一個屏幕有两个地址需要设置。一个是显示屏起始地址origin但寄存器是个16位的(分为两个8位寄存器,下同)所以填的是origin – 0xB8000。另一个地址是當前光标的位置pos寄存器也是16位的,所以填的是pos – 0xB8000

        注意:显示屏的坐标与通常的坐标不一样,这里的坐标原点在左上角与Java Swing中的界面的唑标语义类似。如下图:


上述的原点是其实就是origin我们可以通过改变origin,也就是改变起始地址来改变显示的内存区域实现滚屏的效果。事實上显存是非常大的,通常是0xB8000–0xBFFFF而显示屏显示的只是显存的冰山一角,这里把显存单独作为一个tty设备了其实可以把显存划分为几块,只有一个键盘输入对应设置多个tty,这样也就有了多个互不干扰的控制终端通过按键Ctrl+Alt F1-F7,分别进入不同的tty设备设置该设备对应的显示屏地址和光标当前位置,实现多用户登录的功能把内容写到当前光标位置pos(已经是指针),若落在当前[origin,src_end)里面就可以在屏幕看到该linux字符设備src_end=


0xB8000是显存的起始地址,0x3B4是显存的索引寄存器由于显卡端口众多,要访问各个数据寄存器首先应该向端口0x3B4写入索引,表示接下来的数據由该索引对应的寄存器来接收可以填写0-17,也就是最多可以索引17个寄存器选择相应的索引后,通过0x3B5向该索引对应的寄存器写入数据昰8位寄存器。

1213分别用于索引显示屏起始地址的高8位和低81415分别用于索引显示屏光标地址的高8位和低8位。注意这里都是以linux字符设备为單位需要除以2

这个函数主要是在write_q有数据的情况下将四个中断允许位中的写中断允许位(位1)置位。这个中断允许寄存器是0x3F9(主串口)或者0x2F9(辅串口)这样的话,以后串口准备好时就会自动把数据写到数据口中(0x3F8或者0x2F8)。

4.1、键盘输入的驱动程序

通过前面的讨论我們已经知道了将数据写到显存中,就可以在显示屏显示数据但依旧不知道这些数据是怎么获取到的,或者说键盘的输入是怎么处理的洳何读取串口中的数据。聪明的读者不难发现tty_read读取的数据其实是保存在secondary辅助队列中的,那么secondary这个队列中的数据是怎么来的呢是通过中斷例程自动获取的。每次有数据到达就会产生中断,如键盘中断IRQ133号中断)串口1(主串口)的中断IRQ436号中断,串口2(辅串口)的中断IRQ335号中断

        这里设置的是一个陷阱门,键盘中断时其他中断会被自动关闭也就是在执行键盘中断例程时不允许其他中断的执行。

键盘某個键按下时会产生make扫描码松开时会产生break扫描码。对于同一个按键这两个码是有关系的,就是make码的最高位置1则是break码这样刚好有256个扫描碼。大部分按键产生的扫描码只有一个字节但少数几个按键有两个字节,如RCtrlmake扫描码有两个字节第一个是0xE0,而Pausemake6个字节且第一个昰0xE1。通常我们只在乎make码也就是按下的码。

从上面的函数可以看出键盘的数据口是0x60。先从数据口读取数据然后调用以扫描码为下标的key_table數组中的函数。调用完成后则会操作0x61端口先禁止键盘再允许键盘,以对收到扫描码做出应答最后会调用do_tty_interrupt(0),对数据进行处理并填到secondary队列中。

其中key_table位于同一个文件的第502行调用的函数大都是do_self。我们也可以看到索引128以上大部分是调用none也就是忽略。其他处理函数则是对mode的比特位进行相应设置如左shift键按下,则mode的最低位置一松开则置零。

do_self主要是通过看mode这个字节的比特位看是否有Alt或者Shift键按下(按下不放),進而选择对应的映射表(alt_mapshift_map)否则就选择普通的key_map数组。这三个数组已经在内核代码中且已经初始化,表示的是该键产生的扫描码对应嘚ASCII但是有的键是没有ASCII码的,用零表示直接返回。

上面所有标出颜色的部分都等价于一个if语句共有3个连续的if语句,满足条件则执行第一个加粗部分获得对应的ASCII码,并存放在AL中如果该ASCII码位于[97,125]Caps键或Ctrl键按下,则减去32转化为大写字母这里假设前面用的是key_map这个数组,而苴都是小写字母的ASCII值才能减去32

上面加粗的部分使得EDX获得控制台终端读队列(tty_table[0].read_q)的地址进而将一个linux字符设备AL写入到队头中,并将队头往前移位需要注意的是队头一开始指向的位置为空,可以填充数据而且这里使用的是循环队列,tail== head表示空head+ 1 == tail表示队列已经满了。这里是茬head这个位置先填数据了再判断是否满了。


这个函数是实现行规则的关键主要是对read_q进行遍历,如果是普通linux字符设备则直接复制到tty->secondary中就鈳以了。如果设置了ICANON标志且当前linux字符设备是特殊linux字符设备则对secondary进行处理。如果允许处理信号则根据控制linux字符设备给相关的前台进程组發送对应的信号。同时根据标志还能回显和控制回显等。

在上面定义的tty_table中使用INIT_C_CC这个数组初始化ttytermios结构的控制linux字符设备数组,这个控制linux芓符设备数组保存的是ASCII码上面VEOF即对应这个默认数组的下标。对于EOF这个linux字符设备每个tty都可以自己定义对应的ASCII码也就是对应的是哪个按键。我们可以通过修改控制linux字符设备数组(termios)来更新对应的按键

tty设置规范模式(ICANON)的时候,copy_to_cooked会处理四个特殊linux字符设备删除一行是Ctrl+U,从當前secondary队列的head开始往后删除直到碰到换行或者文件结束符或者secondary队列空为止。删除一个linux字符设备是Ctrl+ H往后移动secondaryhead指针。如果tty有设置回显标志则用一个DEL(ASCII=127)表示删除一个linux字符设备,如果该linux字符设备的ASCII<32则再显示一个DEL输出到write_q中。由于该队列对应的是显示屏所以显示屏还会对其做进┅步的处理,如将光标处的linux字符设备变为空白这样就看不到了。如果是\n则会将光标往前移动一行。同时还对Ctrl+

4.2、串口输入输出的驱动程序

上面函数加粗部分表示获得read_q的地址并通过read_q.data获得数据端口。对于主串口数据端口是0x3F8,而辅串口则是0x2F8之后将通过加2获得中断发生寄存器端口0x3FA。如果该寄存器的最后一位(0位)置空表示有中断。有中断时第1,2位构成四个可能的值对应四种可能的中断,作为索引(EAX已经乘鉯2了)分别执行jmp_table所在处的函数

键盘并没有写操作,所以控制台把键盘和显示屏绑在了一起作为一个可以读写的tty设备。对于write_charECX+4write_q的地址,对该队列进行操作即可数据依旧是发送到0x3F8。注意一次中断只发送一个linux字符设备如果write_q还有linux字符设备则不屏蔽写中断允许,可以继续进荇写否则将0x3F9(设置4个中断允许的寄存器)的第1位置位,表示不允许发生写中断这个位可以在rs_write中被恢复设置。另外两种中断是状态变化的中斷

五、tty设备操作结构图

       注意,读进程没有任何数据到达写进程内核缓冲区已满,都会进入可中断的休眠状态而且是一个链表。条件滿足时会被中断例程唤醒


 Linux中I/O设备分为两类:linux字符设备设备囷块设备两种设备本身没有严格限制,但是基于不同的功能进行了分类。
(1)linux字符设备设备:提供连续的数据流应用程序可以顺序读取,通常不支持随机存取相反,此类设备支持按字节/linux字符设备来读写数据举例来说,键盘、串口、调制解调器都是典型的linux字符设备设备
(2)块设备:应用程序可以随机访问设备数据,程序可自行确定读取数据的位置硬盘、软盘、CD-ROM驱动器和闪存都是典型的块设备,应用程序鈳以寻址磁盘上的任何位置并由此读取数据。此外数据的读写只能以块(通常是512B)的倍数进行。与linux字符设备设备不同块设备并不支持基於linux字符设备的寻址。
总结一下这两种类型的设备的根本区别在于它们是否可以被随机访问。linux字符设备设备只能顺序读取块设备可以随機读取。

我要回帖

更多关于 键盘快捷键使用大全 的文章

 

随机推荐