我在周末写了一篇帖子,说了很多关于图书馆让人失望,以及其他人变得过度依赖图书馆的事情。书中还有一个次要内容,它提到了当您在Unix-ish/POSIX-ish文件系统情况下编写文件时,要向人们讲授所有需要注意的事项。在这种情况下,当您在Unix-ish/POSIX-ish文件系统情况下编写文件时,应该注意的事项都有。一位朋友联系了我,问我是否有帖子谈论这些事情,据我所知,我没有。
这就把我们带到了现在。每当我创建文件时,我都会尝试记下几件事。
首先,write(2)不能保证写入您交给它的全部数据集。首先想到的可能是,当然,磁盘可能已满,但这不完全是我要说的。
不,这是关于更坏更好的整个事情,在这种情况下,当你在某个系统调用中休息,做一些事情(比如写文件)时,可能会被打断。是的,write()可能会被内核戳到头部并提前返回,但它只完成了部分工作。这取决于您是否注意到这一点并重新启动它。
您可能注意到write()返回一个ssize_t,希望大多数阅读这篇文章的人都知道它可能返回的值。最常见的情况是,您将得到请求它写入的字节数。那很好。不太常见的是a-1,意思是有什么东西坏了,现在你不得不卑躬屈膝地四处走动,看看到底发生了什么。
更少的人意识到,在“一切”和“没有”之间有一个中间地带。假设您调用Write并告诉它将16384个字节压入文件描述符。它由于某种原因被中断--可能是信号发出,或者有人碰巧将strace或gdb附加到您的进程。管他呢。
假设返回值为8192,将errno设置为EINTR。您必须注意,这比您最初给它的值(16384)要小,然后再加倍。请注意,您也不能只重新启动您之前执行过的命令,因为您的前半部分数据已经写入!这一次,您必须告诉write()从尚未写入的第一个字节开始,并向下调整计数以匹配。
如果你正在想,这意味着我需要一个指针和一个计数器,而我必须在向前撞击指针并向下抓着计数器的同时循环地做这件事,那么你就走在了正确的轨道上。如果你也在想,嘿,如果一些病态的情况发生,这可能会永远停留在这个循环中,现在你真的是在用汽油做饭。当你的系统在疯狂的情况下保持住,而不是炸毁和拖垮其他所有人的时候,这种偏执就会得到回报。
这里还可以发生其他有趣的事情。也许您正在尝试执行某种循环系统,您可以尽可能快地将数据放入多个文件描述符中,但其中一些文件描述符跟不上您的脚步。(=。也许有些客户比其他客户慢。这会导致整个过程减速到最慢的客户端的速度。也许这会让您考虑非阻塞I/O,这样write()就不会坐在那里等待(网络)缓冲区接受您传递给它的全部内容。
当然,一旦您这样做了,您就会意识到非阻塞I/O有时意味着它不能或不能做您要求的事情,并且会立即返回而不做任何事情,并且errno设置为EAGAIN或EWOULDBLOCK。
如果你现在在想,嗯,我想我需要一个自己的缓冲区,然后我需要回来,稍后再试着写,你说得对!此外,如果你继续沿着这条路走下去,你应该直截了当地说,嘿,最终我需要放弃它们,因为否则,如果它们变得没有反应,我的缓冲区会活生生地吃掉我,这也是一件值得记住的事情。
如果您将这种情况与上面描述的情况(EINTR)结合起来,那么您可能会得出这样的结论:您基本上需要某种环形缓冲区,您可以在其中添加新的传出数据,并向网络(或磁盘、打印机或其他任何设备)提供尚未推出的最旧数据。这意味着内存管理、索引或指针,以及关于什么时候足够就够了的艰难决定,您必须将它们去掉。
但是等等,还有更多!写入并不总是因为简单的原因而失败,它返回-1并将errno设置为类似ENOSPC的值(磁盘已满)。还有一种有趣的东西叫做破裂的管子。这是当您要向未打开读取的管道或已关闭读取端的套接字写入内容时。如果你正在做TCP的事情,回想一下,你实际上有两个不同的流在进行,而另一端可以随时完全停止读取你的信息。
你觉得有什么大不了的。错误号=EPIPE的它是&ll-1,对吗?是的但是。它还会生成SIGPIPE,除非您已经提前显式处理过它。这是正确的,你将得到一个由内核传递给你的充满活力的婴儿信号。你需要一个信号处理程序来接受它并对它做些什么,或者你必须明确地说你不在乎,并将它设置为忽略,但你不能仅仅假装它不会发生。因为它肯定会的。
所以,是的,如果您的程序偶尔会爆裂管道,并将其转储到shell中,尽管您反复发誓您正在处理write()的不良回报,那么,也许这就是原因所在。
还有什么?比赛怎么样?让我们假设两个不同的程序都试图在相同的路径下创建相同的文件。它们都打开/tmp/CoolCool以进行写入。会发生什么事?这取决于打开文件时使用的标志,以及文件是否已经在那里。
如果您传入O_creat,它将在该文件不存在的情况下创建该文件。如果还传入O_EXCL(因此*同时*O_CREAT和O_EXCL),则如果该文件不存在,它将创建该文件;如果该文件已经存在,它将出错。
如果两个程序都试图同时打开同一路径上的O_CREAT|O_EXCL,则一个会赢,另一个会输。这其实是一件好事!这也是你如何发现你在那个地方放了一个新的文件,而实际上没有遵循某个恶人留下的符号链接。
你听到了吗?如果我能让您写入我控制的路径,我也许可以在该位置留下一个符号链接,指向我希望您用某些内容覆盖的文件。也许我可以让你写一些东西,包括我传递给你的一大块数据,在那里我可以放一个符号链接。
如果我将/tmp/target设置为指向/home/you/.ssh/AuthorizedKEYS,会发生什么情况?如果我可以让您编写一些内容,其中可能有一堆废话,但随后将包含一个换行符、一个允许我进入的公钥和另一个换行符,会怎么样?如果您系统允许使用符号链接(某些安全策略不允许这样做),我可能会找到进入您帐户的方法。
人们过去常常通过让root覆盖/etc/rhosts、/etc/dow或您能想到的任何其他内容来做同样的事情。一些系统增加了不遵循全局可写路径(如/tmp和Friends)中不属于同一用户的符号链接的偏执。不要依赖于这一点已经到位了!做正确的事情,并确保重新打开实际的文件。
说到争先恐后地写东西和独家创建文件,让我们来谈谈原子更新。回想一下,write()可能不会一次完成所有工作。您可能需要多次调用它,然后才能完整显示您的内容。那么,如何防止其他进程在中间看到这个写了一半的文件呢?
一种方法是使用锁定。您可以使用flock()、lockf()和fcntl()之类的东西,但要知道,其他所有东西也必须遵循相同的规则。也就是说,另一个进程读取相同的路径,但没有费心查看它被锁定的情况是否会直接向前推进,并看到设法在那里着陆的任何东西。(#**$$=“{#”}{##**$$}{##**$$})。Flock and Friends是建议锁,不是老式的MS-DOS风格,你不能读这个,因为有人在某个地方打开了这个锁。
另一种方法是执行整个原子更新。在这里您可以创建一个临时文件,将所有数据放在其中,然后将其重命名到位。
这可能意味着您要创建/home/you/.Cool.conf.CLXHKJELHFJE,用美味的数据填充它,然后将其重命名为/home/you/Cool.conf。该文件的读者将看到旧文件或新文件,但永远不会看到介于";状态之间的";。
我应该注意到,安全地创建临时文件不是一件容易的事,并且有实际的*系统*库可以正确地做到这一点。尤其是,如果这就是你的全部策略,那么我会打开一条包含我的uid、PID、一天中的时间或随机数字的路径都是废话。您还必须执行O_EXCL|O_CREAT操作,以确保没有人会与您竞争。
换句话说:如果您在临时文件创建中避免种族的想法只是为了拥有一个更宽的名称空间,那么是什么让作恶者(我)不会先抓住每一种可能性呢?磁盘空间很便宜,对吧?你只能有这么多PID,一天中的时间显然是可以预测的,你的随机数空间也同样有限。我可以先把他们每个人都安排好,当你击中的时候,我就赢了!
最后,只要我们讨论的是开放和标志,就不要忘记模式。如果您想创建一个其他人都无法读取的文件,您需要从一开始就这样做。您不能先打开文件然后再chmod(),因为如果有人设法在中间打开它,他们可能会读到它(或者更糟)。
这就是open()采用mode_t的原因,让您告诉它应该拥有什么样的权限。
当然,乐趣并没有到此为止。还有umask,它(顾名思义)在创建文件时屏蔽了一些权限。也就是说,您在umask中设置的位越多,结果文件中设置的位就越少。为什么会有这个?那么,考虑一个程序,它可能想要创建文件0644或0664,这取决于您是自己处理这些内容,还是允许您(Unix)组中的其他人对其进行写入。作为用户,您可以使用umask来选择第一次创建对象时所采用的模式,而无需更改程序。
诚然,我不再认识很多人以这种方式使用这些机器,但在它们真正多用户的日子里,对于某些工作来说,这是有意义的。无论如何,它还在那里,你还是要担心它。
当我说Unix有足够的坑洞和捕熊器来维持整个山谷的运转时,我就是在谈论这类事情。多年来,了解这种废话让我能够兜售我的理智检查服务。当人们(幸运的是这些东西没有占据精神空间)与它发生冲突时,它也会造成无穷无尽的破坏。