open() Call
Beginner–Intermediate
2 of 7
What Does open() Do?
Before your program can read from or write to a file, it needs to open that file. The open() system call is how you do that in Linux. It tells the kernel: “I want to work with this file”. In return, the kernel gives you back a file descriptor — the number you use for all future operations on that file.
What makes open() interesting is the control it gives you through its flags argument. You can tell it whether you want to read, write, or both — and whether to create the file, truncate it, or append to it. This post explains all of that in detail.
The open() Function Signature
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags, ... /* mode_t mode */);
/* Returns: file descriptor (a positive integer) on success
-1 on error (errno is set to explain why) */
Let us break down each parameter:
- pathname — The path to the file you want to open. Can be relative like
"data.txt"or absolute like"/home/user/data.txt". If it is a symbolic link, Linux follows the link to the actual file. - flags — A bitmask that controls how the file is opened. You combine multiple flags using the OR operator (
|). - mode — Only needed when you are creating a new file. It specifies the file permissions (like read/write/execute for owner, group, others).
Access Mode Flags — How You Want to Use the File
Every call to open() must include exactly one of these three flags. Think of this as telling the gatekeeper at the file: “I’m here to read”, “I’m here to write”, or “I’m here to do both.”
| Flag | Meaning | When to Use |
|---|---|---|
O_RDONLY |
Open for reading only | Reading config files, log files, any file you must not change |
O_WRONLY |
Open for writing only | Writing output files, overwriting existing content |
O_RDWR |
Open for both reading and writing | Updating a file in place, random access modifications |
O_RDWR is NOT the same as O_RDONLY | O_WRONLY. Combining O_RDONLY and O_WRONLY with | is a logical mistake and does not give you read-write access. Always use O_RDWR when you need both.
File Creation Flags — Controlling How the File is Created
Without O_CREAT, if the file does not exist, open() fails immediately. With it, Linux creates a brand new empty file for you. When using O_CREAT, you must also provide a mode argument for permissions — otherwise the new file gets random garbage permission bits from the stack.
/* Open for writing, create if not exists, permissions: rw-rw-rw- */
int fd = open("output.txt", O_WRONLY | O_CREAT, 0666);
/* 0666 in octal = rw-rw-rw- */
If the file already exists and has data in it, O_TRUNC truncates (empties) the file to zero bytes before you start writing. Think of it as tearing out all the pages of a notebook before you start writing fresh.
O_TRUNC permanently destroys existing data. Make sure this is what you want before using it. There is no undo.
/* Open existing file, erase all current content, then write fresh */
int fd = open("log.txt", O_WRONLY | O_TRUNC);
/* File is now empty — ready for fresh writes */
Without O_APPEND, writes go to wherever the current file position is — which starts at the beginning. With O_APPEND, every single write is automatically placed at the very end of the file, regardless of where you think the current position is.
This flag is extremely important for log files. Multiple processes may be writing to the same log file simultaneously. With O_APPEND, each write is atomic — the kernel guarantees that different processes’ writes will not overwrite each other.
/* Classic log file pattern — create if needed, always append */
int fd = open("app.log",
O_WRONLY | O_CREAT | O_APPEND,
S_IRUSR | S_IWUSR); /* rw------- */
When you combine O_CREAT | O_EXCL, you are saying: “Create this file, but only if it does NOT already exist. If it does exist, fail.” This is used to prevent accidentally overwriting an existing file, and is common in programs that need to create a unique temporary file or ensure they are the sole creator.
int fd = open("lockfile.pid", O_WRONLY | O_CREAT | O_EXCL, 0644);
if (fd == -1) {
/* File already exists — another instance is running */
perror("Lock file exists");
}
The check for existence and the creation happen as a single atomic operation — there is no gap in between where another process could sneak in and create the file. This makes it safe for multi-process programs.
Complete open() Flags Reference
| Flag | What It Does | POSIX Standard? |
|---|---|---|
O_RDONLY |
Read only | ✓ SUSv3 |
O_WRONLY |
Write only | ✓ SUSv3 |
O_RDWR |
Read and write | ✓ SUSv3 |
O_CREAT |
Create file if it does not exist | ✓ SUSv3 |
O_TRUNC |
Erase existing file content on open | ✓ SUSv3 |
O_APPEND |
All writes go to end of file | ✓ SUSv3 |
O_EXCL |
With O_CREAT: fail if file already exists | ✓ SUSv3 |
O_NONBLOCK |
Open in non-blocking mode (for pipes, sockets) | ✓ SUSv3 |
O_SYNC |
Make writes synchronous (wait for disk) | ✓ SUSv3 |
O_NOFOLLOW |
Do not follow symbolic links | ✓ SUSv4 |
O_NOCTTY |
Do not make this terminal the controlling terminal | ✓ SUSv3 |
O_CLOEXEC |
Close fd automatically when exec() is called (Linux 2.6.23+) | ✓ SUSv4 |
File Permissions — The mode Argument
When you create a new file with O_CREAT, you need to specify who can read, write, and execute the file. Linux has three categories of users for each file:
- Owner (User) — The person who created the file
- Group — Other users who share the same group as the owner
- Others — Everyone else on the system
For each category, you set three permission bits: Read (r), Write (w), Execute (x).
In practice, you usually use the octal numbers (0644, 0755, etc.) because they are shorter and easy to read once you know the pattern. But using the named constants like S_IRUSR | S_IWUSR makes your code more readable and self-documenting.
Practical open() Examples
/* Open an existing file — read only */
/* No mode argument needed since we are not creating */
int fd = open("config.txt", O_RDONLY);
if (fd == -1) {
perror("open"); /* print error message */
exit(1);
}
/* Create a new file, or truncate it if it already exists */
/* Permissions: owner can read+write, no one else can do anything */
int fd = open("output.txt",
O_WRONLY | O_CREAT | O_TRUNC,
S_IRUSR | S_IWUSR); /* 0600 */
if (fd == -1) {
perror("open");
exit(1);
}
/* Open log file for writing, create if needed, always append */
int fd = open("server.log",
O_WRONLY | O_CREAT | O_APPEND,
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); /* 0660 */
if (fd == -1) {
perror("open log");
exit(1);
}
/* Open for both reading and writing — file must exist */
int fd = open("database.dat", O_RDWR);
if (fd == -1) {
perror("open");
exit(1);
}
/* Now you can both read from and write to this fd */
Which Number Will open() Return?
Linux guarantees that a successful open() always returns the lowest unused file descriptor number for your process. Since fd 0, 1, and 2 are already taken (stdin/stdout/stderr), the first file you open typically gets fd 3, the second gets fd 4, and so on.
This rule can be deliberately used. For example, to redirect stdin to a file, you first close fd 0, then open a file — it will get fd 0 because that is now the lowest available number.
/* Trick: redirect stdin to a file */
close(0); /* close fd 0 (stdin) */
int fd = open("input.txt", O_RDONLY);
/* fd is now 0 — programs reading from stdin now read from input.txt! */
When open() Fails — Understanding Errors
When open() fails, it returns -1 and sets the global variable errno to a code that explains what went wrong. Always check for failure and handle errors in real programs.
| Error Code | What It Means |
|---|---|
EACCES |
Permission denied — you do not have the right permissions to open the file this way |
ENOENT |
File not found — the file does not exist and you did not use O_CREAT |
EEXIST |
File already exists — you used O_CREAT | O_EXCL but the file was already there |
EISDIR |
Is a directory — you tried to open a directory for writing |
EMFILE |
Too many open files — your process has hit its limit on open file descriptors |
ENFILE |
System-wide limit on open files has been reached |
EROFS |
Read-only file system — you tried to open a file for writing on a read-only mount |
#include <errno.h>
#include <string.h>
int fd = open("important.txt", O_RDONLY);
if (fd == -1) {
/* perror() prints: "open: No such file or directory" */
perror("open");
/* OR check errno manually: */
if (errno == ENOENT) {
printf("File not found!\n");
} else if (errno == EACCES) {
printf("Permission denied!\n");
}
exit(1);
}
The creat() System Call — Historical Note
Before modern UNIX, the open() call only took two arguments and could not create files. A separate call called creat() was used to create new files. It is still available today for backwards compatibility.
/* Old style: */
int fd = creat("newfile.txt", 0644);
/* This is exactly equivalent to: */
int fd = open("newfile.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
Notice that creat() always opens for writing only and always truncates. You cannot open for reading or use other flags. This is why creat() is considered obsolete — open() with flags gives you much more control. You may still see creat() in older code though.
Key Takeaways from This Post
open()opens (or creates) a file and returns a file descriptor- You must specify exactly one access mode:
O_RDONLY,O_WRONLY, orO_RDWR - Additional flags like
O_CREAT,O_TRUNC,O_APPEND,O_EXCLgive fine-grained control - When creating files with
O_CREAT, always provide amodeargument for permissions - Always check the return value — if it is -1, open() failed and errno tells you why
creat()is an older, obsolete system call — useopen()instead
Up Next: Reading Files with read()
You have opened a file and have a file descriptor. Now it is time to actually read the data from it. The next post covers the read() system call in depth.
Next Post: read() →
