21

I have a situation like the following:

int fds[2];
pipe(fds);
// Write lots of stuff into fds[1]

close(fds[1]);
do_stuff(fds[0]);
// Should I close fds[0] here or at the end of do_stuff?

Which one is better to do?

8
  • 29
    For simplicity of data flow analysis, I would always recommend opening and closing in the same function... you can pass it to sub-functions as required. But there are exceptions to most rules...
    – Andrew
    Oct 28, 2023 at 20:29
  • 1
    "fds" = file descriptors Oct 29, 2023 at 11:52
  • 1
    pipe() is probably: "A pipe is created with the POSIX pipe() function (from <unistd.h>)." (the C POSIX library). Related: fork() Oct 29, 2023 at 12:32
  • 8
    However you choose to do it, make sure you document it.  In particular, the documentation on your function should clearly specify whether it will close the file descriptor, or not (and hence the caller should do so).
    – gidds
    Oct 29, 2023 at 14:56
  • 2
    Presumably, do_stuff doesn't know how the file descriptor it receives was created, and as such doesn't know that no one else will use it later.
    – chepner
    Oct 30, 2023 at 20:47

4 Answers 4

67

In most languages, including C, we usually produce the most maintainable code by dealing with acquire / free at the same level:

    fd = open(...)
    consume(fd);
    close(fd);

This applies to locks, database connections, malloc'd memory, and many other resources. We strive to support "local analysis" by some poor maintenance engineer who wants to read and understand the invariants without needing to vertically scroll the code.


Real C code would need to examine fd and errno for errors, and perhaps goto fail. Other languages may let you use exceptions to ensure that contracts are satisfied and that a close() finally happens. Take advantage of your language's support for cleanup. For example golang offers defer, while python offers with context handlers.


Sometimes resources carry global implications. For example, malloc'ing a kilobyte is no big deal, but allocating a gigabyte might be; it counts against the global RAM resource. And if the consumer needs to sequentially perform N "large" operations, each requiring more than 1/N-th of total RAM, then violating the usual layering constraint might be warranted. But this should be more the exception than the rule.

2
  • Can you please expand on the part where " the consumer needs to sequentially perform N "large" operations"? I don't seem to get the idea. What would be the issue with performing these N large operations sequentially by malloc'ing, consuming, and then free'ing the needed memory at each iteration? Oct 31, 2023 at 11:26
  • 4
    @MehdiCharife I can't speak for all sorts of software, but for the specific case I handled - generating large chunks of simulated landscapes - it had several recursive steps that needed to allocate memory themselves. Recursively allocating memory needs some... special engineering to make work, and this usual format doesn't handle it that well.
    – T. Sar
    Oct 31, 2023 at 11:31
25

Open file descriptors, like allocated memory, are resources¹ which should be clearly owned by a block of code or by some other owned resource.

In C², we need to be absolutely explicit about the ownership of resources at all times, so we can ensure that they are released exactly once each, and never accessed after that release operation.

If we really must transfer ownership from one function to another, we need to ensure that everyone reading either function is aware of the ownership semantics, both in the written documentation and in the code (it helps if we use a documentation-generating tool to ensure they stay consistent):

/**
 * @brief  Create a new list.
 * @return Pointer to the newly-created List, or null pointer on failure.
 * Caller is responsible for deallocating the List, using list_free().
 */
struct List *list_alloc();

/**
 * @brief      Free all memory associated with a list.
 * @param list The list to free.
               Caller must not access list after freeing it.
 * If list is a null pointer, no action is taken
 */
void list_free(struct List *list);

As you can see, transfer of ownership can be onerous, and the more we do it, the harder it is to reason about our program's operation. So clearly we should strive to avoid doing so.

My guidelines

  • Where possible, release resources in the same scope as they are allocated.
  • This may lead to creating acquire/release function pairs as in my example.
  • When it's necessary to store or transfer a resource, we need to take extra care to ensure that future readers are aware that it's happening, and can reason about the resource's ownership and lifetime.

¹ These aren't the only resources - there are other objects which need to be treated in the same manner.

² Many newer languages have mechanisms to make this explicit in the code. For example, C++ has smart pointers and other types which manage ownership, and Java has a garbage collector to release abandoned resources.

5
  • 8
    Though non-reference-counting GC only works well with fungible resources, like memory. A GC is a tool to simulate infinite memory, not to free resources. Oct 29, 2023 at 14:00
  • 6
    Adding to footnote ², Rust is the gold standard here. Passing a value by moving versus passing a value by reference are distinguished lexically and enforced at compile time. In other words, there are different argument types when the callee is expected to clean up the resource versus when the caller is expected to clean up the resource.
    – Nayuki
    Oct 30, 2023 at 2:55
  • 7
    Java has a garbage collector to release abandoned resources - you would never rely on that except for memory allocations, and for more than a decade there's an explicit syntax to close resources at the end of the block that opened them. In 2023 you'd most likely write try(var something=new SomethingAutoCloseable()){/*use something*/}
    – tevemadar
    Oct 30, 2023 at 8:47
  • 1
    To add on the comment by @Nayuki: For the specific case of file descriptors Rust added the types OwnedFd and BorrowedFd in August 2022, to make handling ownership of fds easier.
    – cg909
    Oct 30, 2023 at 16:31
  • Thanks @kiwiron for correcting the edit to what I actually meant! Nov 3, 2023 at 7:48
16

Currently, you have two answers giving you good advice in general. Nevertheless neither of them refers to your specific example, where pipe allocates two file descriptors, which are usually intended to be used by two different processes, one which puts data into a FIFO, and another one which reads the data, asynchronously.

In such a case, it is not uncommon to transfer the ownership for the allocated file descriptors to the other processes and let them close their end of the pipe when they don't need it any more. The process which initiated the pipe may not even know when this happens.

Of course, when you organize your code in a way where

  • process 1 creates the pipe and allocates the descriptors

  • process 1 start process 2 (writer) and process 3 (reader), passing the descriptors accordingly, but still keeping ownership

  • process 1 waits until process 2 and 3 both end

then process 1 can (and probably should) also be the one which closes the files, at the same scope where the files where opened.

8
  • Note that neither process 2 nor process 3 will be able to signal EOF to the other while process 1 has the file descriptors open. Process 1 must close both descriptors so the write process will get a SIGPIPE signal if it writes to the pipe but the reader has terminated or closed the pipe. Similarly, the reader process will not be notified that the writer process has terminated or closed the pipe. That's because in each case, process 1 could write or read data because it has open file descriptors referring to the pipe file descriptors. Oct 30, 2023 at 22:22
  • @JonathanLeffler: process 1 might wait for the end of process 2 and close the write end of the pipe immediately, even when process 3 is running (and vice versa). Gives the same effect as when process 1 just passes ownership of the file descriptors directly to proc 2 and 3 and lets them close the files directly.
    – Doc Brown
    Oct 30, 2023 at 22:32
  • And unless the sub-processes have a mechanism other than encountering EOF on read or failing to write, they will not terminate tidily. In particular, the writer might block waiting for a process to read data from the pipe, but no process is trying to read the pipe. Similarly, the reader might be blocked trying to read from an empty pipe that no process is going to write to. Care is required! Oct 30, 2023 at 22:38
  • @JonathanLeffler: sure, but what do you think does this say about who should close the files?
    – Doc Brown
    Oct 30, 2023 at 23:03
  • It says process 1 should close both ends of the pipe before waiting on either process 2 or process 3. Otherwise, it will most likely be waiting “forever”. Oct 30, 2023 at 23:33
2

The keyword here is ownership. The fact that you are asking this question shows that C doesn't enforce ownership rules. The best rule is "single ownership". Even though C doesn't enforce this, you can still try to use the concept.

For example use the rule "The creator is the owner. Only the owner is allowed to free the resource." That means, like suggested in the other answers, that you make sure to only close the fd in the code block where you created and initialized it. That code block is the owner.

Don't feel bad about being in doubt about this. In fact this is considered a shortcoming of C. C++ provides workarounds for this and Rust enforces single ownership. But in C, it's up to you to apply consistent rules.

Transfer of ownership

In case of pipes, as also explained in another answer, another process may be in charge of freeing the pipe. This is called transfer of ownership. In C, you may want to explicitly document that you are passing ownership and therefore not freeing the resource in the block of creation.

Not the answer you're looking for? Browse other questions tagged or ask your own question.