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?
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.
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.
¹ 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.
try(var something=new SomethingAutoCloseable()){/*use something*/} OwnedFd and BorrowedFd in August 2022, to make handling ownership of fds easier. 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.
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.
do_stuffdoesn't know how the file descriptor it receives was created, and as such doesn't know that no one else will use it later.