The open() System Call — Opening Files in Linux

The open() System Call — Opening Files in Linux
How to open, create, and control files using flags, modes, and the open() system call
Topic
open() Call
Level
Beginner–Intermediate
Part
2 of 7

Keywords:

open() O_RDONLY O_WRONLY O_CREAT O_TRUNC O_APPEND File Permissions creat() errno

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

What open() Looks Like in Code
#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

The Three Basic Access Modes

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
Common Mistake:

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

O_CREAT — Create the File if It Does Not Exist

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- */
O_TRUNC — Wipe Out Existing Content

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.

Warning:

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 */
O_APPEND — Always Write at the End

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------- */
O_EXCL with O_CREAT — Create Exclusively (No Overwrite)

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

All the Flags You Can Pass to open()
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

What are File Permissions?

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).

Permission Bits Layout (rwxrwxrwx)
Owner Group Others r w x r w x r w x 4 2 1 4 2 1 4 2 1 Examples: 0644 = rw- r– r– (owner can read+write, group and others read only) 0755 = rwx r-x r-x (owner can all, group and others can read+execute) 0666 = rw- rw- rw- (everyone can read and write) 0600 = rw- — — (only owner can read and write — private file) POSIX Named Constants (from <sys/stat.h>): S_IRUSR = 0400 (owner read) S_IWUSR = 0200 (owner write) S_IXUSR = 0100 (owner execute) S_IRGRP = 0040 (group read) S_IWGRP = 0020 (group write) S_IROTH = 0004 (others read) S_IWOTH = 0002 (others write)

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

Example 1 — Read an Existing File
/* 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);
}
Example 2 — Create or Overwrite a File
/* 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);
}
Example 3 — Append to a Log File
/* 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);
}
Example 4 — Read and Write an Existing File
/* 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?

The Lowest Available File Descriptor Rule

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

Common open() Errors and What They Mean

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

creat() — The Old Way of Creating Files

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, or O_RDWR
  • Additional flags like O_CREAT, O_TRUNC, O_APPEND, O_EXCL give fine-grained control
  • When creating files with O_CREAT, always provide a mode argument 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 — use open() 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() →

Leave a Reply

Your email address will not be published. Required fields are marked *