3

For my master thesis project I am building an API in C that works with Unix sockets. To make it short, I have two sockets identified by their two fds, on which I have called a O_NONBLOCK connect(). At this point, I am calling select() to check which one connects first and is ready for writing.

The problems start now, as the application which is using this API is aware of only one of those sockets, let's say the one identified by fd1. If the socket identified by fd2 is the first to connect, the application has no way to know it can write to that socket.

I think my best options are using dup() and/or dup2(), but according to the their man page, dup() creates a copy of the fd passed to the function, but which refers to the same open file description, meaning that the two can be used interchangeably, and dup2() closes the new fd which replaces the old fd.

So my assumptions on what would happen are (in 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 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.

Which looks fine, except for the fact that the first dup2() closes fd1, which closes also fd3 since they are identifying the same file description. The second dup2() works fine but it's replacing the fd of a connection which has been closed by the first one, while I want it to keep trying to connect.

Can anyone with a better understanding of Unix file descriptors help me out?

EDIT: I want to elaborate a little bit more on what the API does and why the application only sees one fd.

The API provides to the application the means to call a very "fancy" version of connect() select() and close().

When the application calls api_connect(), it passes to the function a pointer to an int (together with all the necessary addresses and protocols etc). api_connect() will call socket(), bind() and connect(), the important part is that it will write the return value of socket() in the memory parsed through the pointer. This is what I mean by "The socket is only aware of one fd". The application will then call FD_SET(fd1, write_set), call a api_select() and then check if the fd is writable by calling FD_ISSET(fd1, write_set). api_select() works more or less like select(), but has a timer which can trigger a timeout if the connection takes more than a set amount of time to connect (since it's O_NONBLOCK). If this happens, api_select() creates a new connection on a different interface (calling all the necessary socket(), bind() and connect()). This connection is identified by a new fd -fd2- the application doesn't know about, and which is tracked in the API.

Now, if the application calls api_select() with FD_SET(fd1, write_set) and the API realises that is the second connection that has completed, thus making fd2 writable, I want the application to use fd2. The problem is that the application will only call FD_ISSET(fd1, write_set) and write(fd1) afterwards, that's why I need to replace fd2 with fd1.

At this point I'm really confused on whether I really need to dup or just do an integer swap (my understanding of Unix file descriptors is just a little bit more than basic).

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Muffin
  • 55
  • 6
  • 4
    "the first dup2() closes fd1, which closes also fd3" - have you actually verified that that's the case? The fact that `fd1` and `fd3` point to the same open file does **not** mean that closing one automatically closes the other. If that were the case, redirections would be impossible. – user4815162342 Jan 09 '19 at 18:42
  • 1
    What do you mean "aware"? A low-level file descriptor is just a number. You can copy it over. – tadman Jan 09 '19 at 18:45
  • API sounds error prone; what is when application makes `dup(fd1)` before `api_select()` and works on this copy? If possible you should change the API so that e.g. `api_select()` returns an fd, and/or provide functions operating on your fd (e.g. `api_read()`) – ensc Jan 09 '19 at 20:29
  • The application doesn’t call dup(), api_select() does. Moreover api_select() needs to be transparent to the application that why it returns the same codes that select() would return. – Muffin Jan 09 '19 at 20:37
  • https://en.wikipedia.org/wiki/File_descriptor and https://stackoverflow.com/q/5256599/1358308 have reasonable descriptions of how file descriptors work… I'd suggest just keeping track of the "right" file descriptor, rather than using `dup*()` functions which tend to be useful when you're implementing a shell and need to make sure that "FD1" is actually standard input – Sam Mason Jan 09 '19 at 21:58

1 Answers1

4

I think my best options are using dup() and/or dup2(), but according to the their man page, dup() creates a copy of the fd passed to the function, but which refers to the same open file description,

Yes.

meaning that the two can be used interchangeably,

Maybe. It depends on what you mean by "interchangeably".

and dup2() closes the new fd which replaces the old fd.

dup2() closes the target file descriptor, if it is open, before duping the source descriptor onto it. Perhaps that's what you meant, but I'm having trouble reading your description that way.

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

Good so far.

dup2(fd2, fd1); // The description identified by fd2 is now identified by fd1, the description previously identified by fd1 (and fd3) is closed

No, the comment is incorrect. File descriptor fd1 is first closed, and then made to be a duplicate of fd2. The underlying open file description to which fd1 originally referred is not closed, because the process has another open file descriptor associated with it, 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,

Yes it does.

which closes also fd3

No it doesn't.

since they are identifying the same file description.

Irrelevant. Closing is a function on file descriptors, not, directly, on the underlying open file descriptions. In fact, it would be best not to use the word "identifying" here, for that suggests that file descriptors are some kind of identifier or alias for open file descriptions. They are not. File descriptors identify entries in a table of associations with open file descriptions, but are not themselves open file descriptions.

In short, your sequence of dup(), dup2(), and dup2() calls should effect exactly the kind of swap you want, provided that they all succeed. They do, however, leave an extra open file descriptor hanging around, which would yield a file descriptor leak under many circumstances. Therefore, don't forget to finish up with a

close(fd3);

Of course, all that assumes that it is the value of fd1 that is special to the application, not the variable containing it. File descriptors are just numbers. There is nothing inherently special about the objects that contain them, so if it is the variable fd1 that the application needs to use, regardless of its specific value, then all you need to do is perform an ordinary swap of integers:

fd3 = fd1;
fd1 = fd2;
fd2 = fd3;

With respect to the edit, you write,

When the application calls api_connect(), it passes to the function a pointer to an int (together with all the necessary addresses and protocols etc). api_connect() will call socket(), bind() and connect(), the important part is that it will write the return value of socket() in the memory parsed through the pointer.

Whether api_connect() returns the file descriptor value by writing it through a pointer or by conveying it as or in the function's return value is irrelevant. The point remains that it is the value that matters, not the object, if any, containing it.

This is what I mean by "The socket is only aware of one fd". The application will then call FD_SET(fd1, write_set), call a api_select() and then check if the fd is writable by calling FD_ISSET(fd1, write_set).

Well that sounds problematic in light of the rest of your description.

[Under some conditions,] api_select() creates a new connection on a different interface (calling all the necessary socket(), bind() and connect()). This connection is identified by a new fd -fd2- the application doesn't know about, and which is tracked in the API.

Now, if the application calls api_select() with FD_SET(fd1, write_set) and the API realises that is the second connection that has completed, thus making fd2 writable, I want the application to use fd2. The problem is that the application will only call FD_ISSET(fd1, write_set) and write(fd1) afterwards, that's why I need to replace fd2 with fd1.

Do note that even if you do swap file descriptors as described in the first part of this answer, that will have no effect on either FD's membership in any fd_set, for such membership is logical, not physical. You will have to manage fd_set membership manually if the caller relies on that.

It is unclear to me whether api_select() is intended to provide services for more than one (caller-specified) file descriptor at the same time, as select() can do, but I imagine that the bookkeeping required for it to do so would be monstrous. On the other hand, if in fact the function handles only one caller-provided FD at a time, then mimicking the interface of select() is ... odd.

In that case, I would strongly urge you to design a more suitable interface. Among other things, such an interface should moot the question of swapping FDs. Instead, it can directly tell the caller what FD, if any, is ready for use, either by returning it or by writing it through a pointer to a variable specified by the caller.

Also, in the event that you do switch, one way or another, to an alternative FD, do not overlook managing the old one lest you leak a file descriptor. Each process has a pretty limited quantity of those available, so a file descriptor leak can be much more troublesome than a memory leak. In the event that you do switch, then, are you sure you really need to swap, as opposed to just dup2()ing the new FD onto the old, then closing the new?

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • Thanks for your answer. The last part is somewhat confusing to me, so I want to elaborate a little bit more on what the API does. Please see my EDIT in the question. – Muffin Jan 09 '19 at 20:09
  • @Muffin, I have added commentary directed toward your edit. – John Bollinger Jan 09 '19 at 21:47
  • Ok I can see where the problems can arise, I just want to clarify the workings of `api_select()`. In the API every fd is just a member of a much more complex data structure called socketlist, which as the name implies is a linked list. The first element of the list has the fd the caller is aware of, all the new connection attempts with their fd follow. When api_select() gets called it goes through the list and before the actual select() calles FD_SET() on every fd that's in the list. So if for example fd2 is the one that conneced FD_ISSET(fd2, write_set) will be true. – Muffin Jan 09 '19 at 22:20
  • Fine, @Muffin, but after `select()` returns, only those file descriptors that it determined were ready will be set any longer. If you *then* swap file descriptors, the caller will not see the expected FD set, even though, courtesy of the swap, it actually is ready. – John Bollinger Jan 09 '19 at 22:35
  • Sorry it took so long to actually update you on the matter. Anyway, I know that trying to explain how my APIs work would be impossible, but I wanted to let you know that doing what I had in mind worked with some adjustments I've done thanks to your answer. Thanks! – Muffin Feb 14 '19 at 20:43