如何交换两个打开的文件描述符?

3
对于我的硕士论文项目,我正在使用C语言构建一个与Unix套接字配合工作的API。简单来说,我有两个由它们各自的文件描述符所标识的套接字,在这两个套接字上我调用了O_NONBLOCK connect()。在这个点上,我调用了select()来检查哪一个先连接并准备好进行写入。
问题现在开始了,因为使用此API的应用程序只知道其中一个套接字,假设是由fd1标识的那个。如果由fd2标识的套接字首先连接,应用程序就无法知道可以向那个套接字进行写入。
我认为最好的选择是使用dup()和/或dup2(),但据其手册所述,dup()会创建传递给函数的文件描述符的副本,但是二者引用相同的打开文件描述符,这意味着两者可以互换使用,而dup2()则关闭替换旧文件描述符的新文件描述符。所以我对会发生什么做了如下假设(伪代码):
int fd1, fd2, fd3;

fd1 = socket(x); // what the app is aware of
fd2 = socket(y); // first to connect

fd3 = dup(fd1); // fd1 and fd3 identify the same description
dup2(fd2, fd1); // The description identified by fd2 is now identified by fd1, the description previously identified by fd1 (and fd3) is closed
dup2(fd3, fd2); // The description identified by fd3 (copy of fd1, closed in the line above) is identified by fd2 (which can be closed and reassigned to fd3) since now the the description that was being identified by fd2 is being identified by fd1.

看起来很好,但第一个dup2()关闭了fd1,因为它们标识着相同的文件描述符,所以也关闭了fd3。第二个dup2()运行良好,但它替换了已被第一个关闭的连接的fd,而我希望它继续尝试连接。
有没有对Unix文件描述符有更好理解的人可以帮帮我?
编辑:我想进一步阐述API的功能以及应用程序为什么只看到一个fd。
API提供给应用程序调用非常“高级”的connect()select()close()的方式。
当应用程序调用api_connect()时,它将向函数传递一个指向int的指针(以及所有必要的地址和协议等)。api_connect()将调用socket()bind()connect(),重要的是它将把socket()的返回值写入通过指针解析的内存中。这就是我所说的“套接字只知道一个fd”的意思。然后,应用程序将调用FD_SET(fd1, write_set),调用api_select(),然后通过调用FD_ISSET(fd1, write_set)检查fd是否可写。api_select()的工作方式与select()差不多,但它有一个计时器,如果连接花费的时间超过一定的时间(因为它是O_NONBLOCK),就会触发超时。如果发生这种情况,api_select()将在不同的接口上创建一个新的连接(调用所有必要的socket()bind()connect())。这个连接由一个新的fd-fd2-标识,应用程序不知道,并在API中跟踪。
现在,如果应用程序使用FD_SET(fd1, write_set)调用api_select()并且API意识到这是第二个已完成的连接,从而使fd2可写,我希望应用程序使用fd2。问题是应用程序只会在之后调用FD_ISSET(fd1, write_set)write(fd1),这就是为什么我需要用fd1替换fd2的原因。
此时,我真的很困惑是否需要dup或者只需要整数交换(我对Unix文件描述符的理解仅仅比基础知识多一点)。

4
“第一个dup2()关闭fd1,也会关闭fd3” - 你是否实际验证了这一点?事实上,fd1fd3指向同一个打开的文件,并不意味着关闭其中一个会自动关闭另一个。如果是这样的话,重定向操作将无法实现。 - user4815162342
1
你说的“意识到”是什么意思?低级文件描述符只是一个数字,你可以复制它。 - tadman
API听起来容易出错;如果应用程序在api_select()之前进行dup(fd1)并在此副本上工作,会发生什么?如果可能的话,您应该更改API ,使例如api_select()返回一个fd,并/或提供操作您的fd的函数(例如api_read())。 - ensc
应用程序不调用dup(),而是由api_select()调用。此外,api_select()需要对应用程序透明,因此它返回与select()相同的代码。 - Muffin
https://en.wikipedia.org/wiki/File_descriptor和https://dev59.com/xW435IYBdhLWcg3wuicn提供了关于文件描述符如何工作的合理描述...我建议只跟踪“正确”的文件描述符,而不是使用`dup*()`函数,这些函数在你实现一个shell并需要确保“FD1”实际上是标准输入时非常有用。 - Sam Mason
1个回答

4
我认为我最好的选择是使用dup()和/或dup2(),但根据它们的手册,dup()创建传递给函数的文件描述符的副本,但是它引用相同的打开文件描述符,这意味着两者可以互换使用。也许吧。这取决于您对“互换使用”一词的理解。而dup2()则会关闭替换旧文件描述符的新文件描述符。如果目标文件描述符已经打开,则dup2()会先关闭它,然后再将源文件描述符复制到目标文件描述符上。也许这就是您的意思,但我很难以这种方式阅读您的描述。

So my assumptions on what would happen are (excuse my crappy pseudo code)

int fd1, fd2, fd3;

fd1 = socket(x); // what the app is aware of
fd2 = socket(y); // first to connect

fd3 = dup(fd1); // fd1 and fd3 indentify the same description

到目前为止还不错。

dup2(fd2, fd1); // The description identified by fd2 is now identified by fd1, the description previously identified by fd1 (and fd3) is closed
不,这个评论是错误的。文件描述符fd1首先被关闭,然后被复制成fd2。原始引用fd1的底层打开文件描述符未被关闭,因为该进程有另一个与之关联的打开文件描述符fd3。
dup2(fd3, fd2); // The description identified by fd3 (copy of fd1, closed in the line above) is identified by fd2 (which can be closed and reassigned to fd3) since now the thescription that was being identified by fd2 is being identified by fd1.

Which looks fine, except for the fact that the first dup2() closes fd1,

是的,它可以。

这也关闭了fd3。

不,它不行。

因为它们正在识别相同的文件描述符。

无关紧要。关闭是针对文件描述符而不是底层打开的文件描述符的功能。实际上,在此处最好不要使用“识别”这个词,因为这暗示文件描述符是打开文件描述符的某种标识符或别名。它们不是。文件描述符标识与打开文件描述符相关联的表中的条目,但它们本身并不是打开的文件描述符。

简而言之,您的dup()dup2()dup2()调用序列应该完全实现您想要的交换,前提是它们都成功了。然而,它们会留下一个额外的打开文件描述符,这可能会导致文件描述符泄漏。因此,请不要忘记完成以下操作:

close(fd3);

当然,这一切都假设应用程序所特别关注的是fd1而非包含它的变量。文件描述符只是数字,与其相关联的对象并没有什么特别之处。因此,如果应用程序需要使用变量fd1,而不管它的具体值是多少,那么你只需要执行一个普通的整数交换即可。
fd3 = fd1;
fd1 = fd2;
fd2 = fd3;

关于编辑部分,你写道:
当应用程序调用api_connect()时,它会将指向int的指针(以及所有必要的地址和协议等)传递给函数。 api_connect()将调用socket()、bind()和connect(),重要的是它将在通过指针解析的内存中写入socket()的返回值。
无论api_connect()是通过将文件描述符值通过指针写入还是作为或在函数的返回值中传递它都没有关系。重点仍然是值而不是包含它的任何对象。
这就是我所说的“套接字只知道一个fd”的意思。应用程序将调用FD_SET(fd1, write_set),调用api_select(),然后检查是否可写,方法是调用FD_ISSET(fd1, write_set)
在您的描述的其余部分的情况下,这听起来有问题。
在某些条件下,api_select()创建了一个新连接并使用不同的接口(调用所有必要的socket()、bind()和connect())。此连接由一个新的fd-fd2-标识,应用程序不知道,并在API中跟踪。
现在,如果应用程序使用FD_SET(fd1, write_set)调用api_select(),并且API意识到这是已完成的第二个连接,因此使fd2可写,我希望应用程序使用fd2。问题在于应用程序只会在之后调用FD_ISSET(fd1, write_set)write(fd1),这就是为什么我需要用fd1替换fd2的原因。
请注意,即使您按照本答案第一部分所述交换文件描述符,这也不会对任何fd_set成员身份产生影响,因为这种成员身份是逻辑而不是物理的。如果调用者依赖于这一点,则必须手动管理fd_set成员资格。
我不清楚api_select()是否旨在同时为多个(调用者指定的)文件描述符提供服务,就像select()一样,但我想它所需的簿记工作将是庞大的。另一方面,如果实际上该函数一次只处理一个调用者提供的FD,则模仿select()的界面是...奇怪的。
在这种情况下,我强烈建议您设计一个更合适的接口。其中,这样的接口应该可以消除关于交换FD的问题。相反,它可以直接告诉调用者哪个FD(如果有的话)可以使用,可以通过返回它或通过写入指向由调用者指定的变量的指针来实现。
另外,在您切换到另一个FD时,不要忘记管理旧的FD,以免泄漏文件描述符。每个进程可用的文件描述符数量非常有限,因此文件描述符泄漏可能比内存泄漏更麻烦。在您切换时,您确定您真的需要交换吗?还是只需将新的FD复制到旧的FD上,然后关闭新的FD?

谢谢你的回答。最后一部分对我来说有些困惑,所以我想更详细地阐述一下API的作用。请查看我在问题中的编辑。 - Muffin
@Muffin,我已经添加了针对你的编辑的评论。 - John Bollinger
好的,我可以看出问题可能出在哪里,我只想澄清 api_select() 的工作原理。在 API 中,每个 fd 只是一个名为 socketlist 的更复杂数据结构的成员,正如其名称所示,它是一个链表。列表的第一个元素具有调用者知道的 fd,所有新的连接尝试及其 fd 都会跟随。当调用 api_select() 时,它会遍历整个列表,并在实际调用 select() 之前对列表中的每个 fd 调用 FD_SET()。因此,例如,如果 fd2 是连接的那个,则 FD_ISSET(fd2, write_set) 将为 true。 - Muffin
好的,@Muffin,但是在select()返回后,只有那些被确定为就绪的文件描述符才会继续设置。如果你然后交换文件描述符,调用者将无法看到预期的FD集合,即使由于交换,它实际上已经准备好了。 - John Bollinger
很抱歉让你等了这么久才更新事情。无论如何,我知道试图解释我的API如何工作是不可能的,但我想让你知道,由于你的回答,我所想做的事情已经通过一些调整得以实现。谢谢! - Muffin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接