扒拉一下UNIX/Linux select系统调用的历史

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/dog250/article/details/100591091

最近扒垃圾桶有点上瘾了…

哦,不,谁说这些是垃圾了,这些只是历史久远了而已。互联网时代的从业人员总觉得什么都是新的,这并不正确,这也很悲哀!

虽然没有UNIX/Linux fork那般久远,本文的主角select也是一个直到今天我们还在使用的系统调用。

由于周末白天的时间并不完全属于自己,只能长话短说。


1969年,UNIX诞生。

1970年代,UNIX获得了蓬勃的发展,到了1980年代,不过在此很早之前,UNIX就抽象出了 “一切皆文件” 的思想。

一切皆文件是一个很典型的适配器设计模式的实例,不管底层是什么,是磁盘文件,是管道,还是键盘鼠标,或者显示器,在UNIX系统中都表示成文件,区分它们的方式是定义不同的 读和写 规则。

后来,Sun公司把上面这段话做成了一个叫做VFS(Virtual file system)的抽象层并实现了一个实例,这是1985年的事了,我们关于select的故事在那之前,大约1980年吧,那时的UNIX远没有这么规整,虽然接口很perfect,思想也还不错,但是实现还远谈不上优美。

select来源于基于文件描述符的全双工通信的需求。 这个需求在socket出现之后尤甚。

先从最初的情况说起,大概就是1969年,1970年前后吧。UNIX系统shell就是一个普通的半双工本地IO。操作员写终端,操作员读终端两个方向的操作是不能同时进行的。

这没什么不合理的,毕竟对端是个机器,它没有智能,它只能根据操作员的指令而行动:

  • 操作员发出指令:写文件描述符。
  • 机器返回结果:读文件描述符。

机器不能在操作员发出指令前给出结果。所以,半双工就够了,UNIX的shell一直也都是这样做的:

void loop()
{
	while(1) {
		cmd = read_tty(); // read
		if (fork() == 0) {
			exec(cmd); //write
		}
		wait();
	}
}

然而,文件描述符的对端要是个人怎么办?

cu命令的出现呈现了这样一种不同的诉求。cu命令本身不重要,简单点说,cu就是打电话。打电话是个全双工操作:

  • 你在说话的时候,对方可能也在说话,比如吵架的场景。

UNIX遇到这种场景,还是可以应对的,这个全双工通信需求第一次挑战了UNIX哲学,解决方案的事实证明UNIX足够强大:

  • 一切皆文件配合fork。

以下是一个典型的全双工通信场景,打电话的伪代码:

void phone()
{
	int fd;
	fd = dial(...);
	if (fork() == 0) {
		while (1) {
			word = read(fd, $我收听的话);
			... // 理解这句话
		}
	} else {
		while (1) {
			... // 准备回复的言语
			talk = write(fd, $我要说的话);
		}
	}
}

貌似还不错。

现在考虑一种混合的场景,考虑传话中继的工作:

  • 接线员监听N个终端:监听终端。
  • 接线员听某个终端:读终端。
  • 接线员把听到的话传给另一个终端:写终端。

根据UNIX文件的传统特性,如果一个文件描述符代表的文件没有数据可读,那么读文件操作将会阻塞,传话中继接线员并不知道N个终端到底哪个终端可读,随便读一个终端都会造成进程阻塞。

假设终端1的数据2分钟后才会到来,中继接线员不巧让进程去读终端1,这会造成终端1阻塞2分钟…在10秒后,终端2来了数据,然而此时进程已经阻塞在了终端1,只能兴叹!

中继接线员迫切需要一个指示:

  • 告诉我哪个终端有数据可读!

这个事情可以通过IPC来解决,但是这都是后话,那个时候还没有IPC…换句话说,如果说要实现这个需求,IPC是一个有力的竞争者,毕竟那个时候System V已经有了几个可用的IPC了,但是对于产生socket的BSD,却没有好用的IPC。

select来也!select是典型的 “需要做什么,我就做什么” 的代表,它所做的不多也不少。select就是来救场的,还不错,这个救场动作足够帅!

select会一并检查所有N个终端,然后返回有数据可读的终端的列表。

如果没有任何终端有数据可读,select将会阻塞,只要有任何终端到来数据,select就会返回。

select系统调用非常有用,它解决了全双工通信场景下的几乎所有 “由于不知情而盲目阻塞” 的问题。

1983年,TCP/IP登场,随后BSD socket打败System V Streams成为了标准,说白了socket就是一个扁平化的全双工文件描述符,没有Streams那样重度依赖ioctl,这是socket的优势,同时促进了select成为标准。

至于说后面的poll,epoll,那不过是针对select的优化而已。


但是这是故事的全部吗?貌似远远没有fork的故事精彩。确实,我也觉得,无论从UNIX进程还是从TCP/IP的角度,select都是配角,但正是这个select融合了二者。

回到fork方案,一个文件描述符分别由两个进程来读写,然而,在UNIX的进程抽象里,两个进程是隔离的,在前IPC年代,它们显然不好过于阴阳两隔。

既然不能让多个进程协同,那就在一个进程里完成所有。 这就是select!

历史总是让人觉得惊奇,不知是必然还是有人故意剔除了不精彩的部分。

BSD和System之间当时1V1。

BSD产生了socket,而System V则有IPC,二者都是为了同一个目标,无奈的是,socket认同了 “一切皆文件” 但System V却另辟蹊径,IPC!

于是对于BSD而言,socket也是文件,对于System V,显然IPC方案就被select给降维打击了,因为select的前提就是“一切皆文件”。


很多人会觉得这没有意义,讲select的历史没有意义,但是持这种观点的人真懂select的实质吗?无非也就是精通库函数用法吧。

有人问为什么UNIX发信号统称为kill这个不太吉利的词,事实上最开始的UNIX系统发送信号就是为了很不吉利的目的。几乎所有的信号默认动作都是结束进程(又要怼了是吧,因为总是能找个例子,信号的默认操作不是结束进程)。看点历史就明白了。

看看我们现在的说法,IPC都有哪些?…,最后别忘了,socket也是IPC的一种。


浙江温州皮鞋湿,下雨进水不会胖!

文章创建于: 2019-09-07 09:47:24
展开阅读全文

没有更多推荐了,返回首页