LWN: 模拟实现iopl()

点击上方蓝色“ Linux News搬运工 ”关注我们~

Emulated iopl()

By  Jonathan Corbet

November 8, 2019

原文来自:https://lwn.net/Articles/804143/

无论是操作系统,还是计算机硬件,都背负着很多历史负担。x86 I/O-port机制就是其中一个,近20年来设计的硬件都很少使用这个机制了,不过我们仍然需要保留对他的支持。不过,这并不意味着我们也不能对它进行清理和改进了,尤其是当这些旧的机制看起来不太合适的时候。这里就有一个例子,来自于Thomas Gleixner的the iopl() patch set。

在大多数体系结构上,I/O都是按照memory-mapped I/O(MMIO)区域来处理的。每个外设都会有一组寄存器可供访问,看起来就像一片内存区域一样,这些区域就会被映射到处理器的地址空间之中。设备驱动程序也就能通过对这些寄存器进行普通内存访问(或者某些类似的方式)来读写,从而跟外设交互。这个机制很有弹性,例如可以把一组寄存器直接映射到某个user-space进程去,如果有需要的话。很多user-space驱动程序都会依赖这个特性。

在x86体系结构的早期,情况跟现在有些不同。当时创建了一个独立的地址空间用来支持65536个I/O端口,需要使用特殊的指令来访问这些I/O端口。哪怕有些设备可以进行内存映射,也是为了其他目的的,至于控制接口还是需要使用I/O端口来访问。用来访问I/O端口的指令都必须是特权级的,因此user-space代码一般不能使用这些指令。

不过前面提过,有时候确实需要在user space来操作外设。为了支持这个功能,x86设计者就创建了两种独立的方式来供对 非特权进程来访问 I/O端口:

  • I/O privilege level (IOPL) 是2个bit的变量,用来控制哪些特权级别的进程可以访问I/O端口。通常这个寄存器设置为0,也就是说只有在kernel mode的代码才能够访问I/O port。如果设置成3的话,就能让普通的user-space进程来访问I/O端口了。这样一来,我们可以通过针对某个特定进程来设置IOPL(可以通过iopl()系统调用来修改),从而让这个进程可以访问所有I/O端口。

  • 在task state segment (TSS)里面存储的I/O port permission bitmap可以用来允许对某个特定Port的操作权限。如果对应某个port的相应bit是0,那么在运行的进程就可以访问这个port。可以使用ioperm()系统调用来操作这个bitmap。

特权进程可以利用iopl()或者ioperm()来获得对I/O端口的访问权限。调用ioperm()的话会增加进程的上下文切换时间,因为8KB bitmap在每次上下文切换的时候都需要进行一次copy。因此,不少程序会使用iopl,尽管它其实并不需要访问那么多的端口。

不过,使用iopl()的话还有其他一个小问题:如果提升I/O privilege level的话,也会允许当前进程打开或者关闭中断。Gleixner认为这个不太好,因为一个恶意程序就能轻松把CPU锁死,只要关掉中断然后做死循环即可。不过真正的问题是,大家设计的时候根本没有考虑过user space会可能关闭中断。kernel开发者一般都假设user space在运行的时候中断都是打开的,而IOPL特升特权的进程其实会违背这个假设,因此进程如果真的去关闭中断的话会有各种未知的问题出现,而目前IOPL方案就有这个问题。

Gleixner认为最好的解决方案就是彻底把iopl()拿掉。不过现在还有不少应用程序是依赖这个功能的,所以不能这么粗暴的拿掉。不过,可能还有一个方案:利用bitmap来模拟iopl()功能。如果某个进程使用I/O privilege bitmap并且清空所有bit,那么就可以访问所有的I/O端口,就跟利用IOPL提升权限是一样的效果,不过就不再能关闭中断了。

如果这么做的话,可能会导致此前一些通过这种方式来实现user space关闭中断的进程无法正常工作。Gleixner搜索了一下是不是有这中程序,唯一找到的一个是一个非常古老的X服务器解决方案,这套代码完全无法在当前系统上运行了,因此不用担心。希望他的搜索结果没有太大遗漏。

可以通过把iopl()替换成bitmap方式来解决中断问题,不过前面提到的性能问题还是没法解决。这个patch set里面实现了一些优化来改善。大多数进程都根本不会用这个bitmap,因此并不需要为他们专门把bitfile设成全1,只要修改一下指向TSS中bitmap的那个指针,改为无效值即可,这样所有访问I/O端口的操作都会被拒绝。在上下文切换的时候,如果切换的这两个进程都在用bitmap,那么也只需要把清0的bit复制过去即可,这样就能大大加快速度。最终效果上来说,模拟iopl()的方案引入的开销是有的,不过也非常接近于0了。

Linus Torvalds指出,可以继续改善性能,不需要去管那个I/O bitmap,直到不得不需要修改的时候再改。这个优化主要是针对那些只有一个进程需要访问I/O端口的场景,其实这个场景占比非常高。Gleixner打算去照此实现。

Willy Tarreau建议再进一步,针对所有调用ioperm()的进程,都使用一个全0的bitmap。这样的缺点是,目前一些进程只打开了某些特定端口的访问权限,这么改过之后会导致它能访问所有端口了。他说,既然调用者进程本来就有权能获取那些端口的访问权限,那么这种改法并不会引入什么安全风险。Eric Biederman则指出DOSEMU其实会依赖ioperm()管控好只有申请过的端口才能访问,因此这个方案不现实。

目前对这个patch set整体来说没有什么反对意见,预计很快会在修改后合入。今后的kernel就能改善这个古老方案的一个缺点了,非常好。

全文完

LWN文章遵循CC BY-SA 4.0许可协议。

极度欢迎将文章分享到朋友圈 

长按下面二维码关注:Linux News搬运工,希望每周的深度文章以及开源社区的各种新近言论,能够让大家满意~

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章