System calls in C — theory with open, read, write, lseek and a saved game written on top of another (and the points that weren’t mine)
Last summer I let my friend Max try a little game I had written in C to practise: it saved the game to a file, save.sav, with one line per player. Max reached level 12 with 2750 points and saved. Then I played, reached level 3 and saved on top. When I loaded the game, it greeted me like this:
Bytes read: 20 Loaded save: Lia level 03 2750
My name, my level… and Max’s 2750 points. No compiler error, no warning. Here is the program, cut down to the essentials:
#include <stdio.h>
#include <fcntl.h> /* open and the O_... constants */
#include <unistd.h> /* read, write, lseek, close */
#include <string.h> /* strlen */
int main(void) {
/* 1. Max's old save: a 20-byte slot */
int fd = open("save.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, "Max level 12 2750\n", 20);
close(fd);
/* 2. I save MY game on top... but without O_TRUNC */
const char *mine = "Lia level 03"; /* only 14 bytes */
fd = open("save.sav", O_WRONLY | O_CREAT, 0644);
write(fd, mine, strlen(mine)); /* overwrites the first 14 */
close(fd);
/* 3. I load the save */
char buf[64];
fd = open("save.sav", O_RDONLY);
ssize_t n = read(fd, buf, sizeof buf - 1);
close(fd);
buf[n] = '\0'; /* read doesn't add the '\0' */
printf("Bytes read: %zd\n", n);
printf("Loaded save: %s", buf);
return 0;
}
Can you spot the bug? The second open is missing O_TRUNC. Without that option, opening a file for writing does not empty it: the pointer starts at byte 0 and write overwrites byte by byte. My save took 14 bytes ("Lia level 03") and Max’s took 20, so the last 6 (" 2750\n") survived. The numbers add up: 20 − 14 = 6 bytes of Max’s, and read read all 20 bytes of the file.
Today you’ll understand why that happens and learn to handle files the way the operating system itself does: with system calls. In the previous article, on processes, you saw that fork() creates a process by asking the kernel. Files work the same way: open, read, write, lseek and close are direct requests to the kernel, without the fopen and fprintf layer. By the end you’ll be ready for the practice, where we’ll save, copy and edit a real saves file.
Table of Contents
Library functions versus system calls
In C there are two ways of working with files:
| Library functions | System calls | |
|---|---|---|
| Examples | fopen, fprintf, fgets, fclose | open, read, write, lseek, close |
| Headers | <stdio.h> | <fcntl.h>, <unistd.h> (and <sys/stat.h> for permissions) |
| What opening returns | A FILE * pointer | An integer: the descriptor |
| Buffer | Yes, the library collects data and writes it in blocks | No: every call goes straight to the kernel |
| They work with | Formatted text (%d, %s) | Raw bytes |
Library functions are built on top of system calls: fprintf ends up calling write. Why learn the lower layer if the upper one is more comfortable? Because it’s the most direct access a program has, because it works the same with files, with the terminal and with anything that sends or receives bytes (devices, pipes, network connections), and because it’s what IC2 asks you about.
The system calls we’re going to use are not standard C but POSIX (Linux, macOS…). They compile without trouble with gcc -Wall -std=c11 on Linux; on Windows you need WSL or a virtual machine.
The headers in this article are always the same: <fcntl.h> brings open and the O_... constants; <unistd.h> brings read, write, lseek and close.
File descriptors: 0, 1, 2… and the next free one
When a process opens a file, the kernel doesn’t give it the file: it gives it an integer, the descriptor. From then on everything is done with that number: read(3, …) means “read from the file I have open as 3”.
Before main starts, every process already has three open descriptors, by convention:
| Descriptor | Name | Usually |
|---|---|---|
| 0 | standard input (stdin) | the keyboard |
| 1 | standard output (stdout) | the terminal |
| 2 | standard error (stderr) | the terminal |
That’s why a program’s first open almost always returns 3: the kernel hands out the lowest free descriptor, and 0, 1 and 2 are already taken. Look:
#include <stdio.h>
#include <fcntl.h> /* open and the O_... constants */
#include <unistd.h> /* read, write, lseek, close */
#include <string.h> /* strlen */
int main(void) {
/* 0, 1 and 2 are already open before main starts */
const char *msg = "Written with write(1, ...)\n";
write(1, msg, strlen(msg)); /* 1 = standard output */
const char *err = "Written with write(2, ...)\n";
write(2, err, strlen(err)); /* 2 = standard error */
/* Each open returns the lowest free descriptor */
int a = open("slot1.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
int b = open("slot2.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
int c = open("slot3.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
printf("slot1 -> fd %d, slot2 -> fd %d, slot3 -> fd %d\n", a, b, c);
close(b); /* 4 becomes free */
int d = open("slot4.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
printf("After closing fd %d, slot4 -> fd %d\n", b, d);
/* A file that does not exist: open returns -1 */
int e = open("slot9.sav", O_RDONLY);
printf("open of slot9.sav returns %d\n", e);
if (e == -1)
perror("Error opening slot9.sav"); /* explains the reason */
close(a);
close(c);
close(d);
return 0;
}
The output of the single run:
Written with write(1, ...) Written with write(2, ...) slot1 -> fd 3, slot2 -> fd 4, slot3 -> fd 5 After closing fd 4, slot4 -> fd 4 open of slot9.sav returns -1 Error opening slot9.sav: No such file or directory
Step by step:
- The first two lines don’t use
printf: they arewrite(1, …)andwrite(2, …). Writing to descriptor 1 is writing to the terminal, without opening anything. Descriptor 2 also ends up on the terminal, but it’s a different channel (if you redirect the output with> output.txt, 2 would still show on screen). slot1,slot2andslot3get 3, 4 and 5: the next free ones, in order.- When 4 is closed, that slot becomes free, and the next
openreuses it:slot4.savgets a 4, not a 6. That’s the “lowest free” rule. slot9.savdoesn’t exist, soopenreturns −1. −1 is how all these calls say “something went wrong”, andperrorturns the reason into text:No such file or directory.
Not checking for −1. If open fails and you carry on, you call read(-1, …), which fails too, and the program goes on with data that was never read. After every open, an if (fd == -1) { perror(...); return 1; }.
The text you pass to perror goes before the colon; what comes after is added by the system according to the error. In the lab terminal it may appear in Spanish if the system is set to Spanish; here you see it in English.
The kernel’s three tables
A descriptor is just an index. So where are the reading position, the opening mode, the size of the file? In three structures the kernel maintains:
- Descriptor table (one per process): position 3 points to an entry of the next table. It’s the only thing that “is” yours.
- Open file table (system-wide): one entry for each
open. It stores the read/write pointer (which byte you’re at) and the mode (O_RDONLY,O_WRONLY…). - I-node table: one entry per file on disk, with its size, permissions, owner and where its blocks are.
Notice two details in the diagram that we’ll check with code at the end of the article: if the same process does two opens of the same file, it has two entries (A and B) with two different pointers; if it does one open and then fork, the child copies the descriptor table, but its fd 3 points to the same entry A, so parent and child share the pointer.
When you don’t understand what a program does with files, ask yourself “how many open-file entries are there and where is each one’s pointer?”. Almost every mystery is solved there.
open: opening (or creating) a file
int open(const char *path, int flags); int open(const char *path, int flags, mode_t mode); /* required with O_CREAT */
flags says how you want to open it. One of the first three is mandatory, and options can be added with the | operator:
| Flag | Meaning |
|---|---|
O_RDONLY | Read only |
O_WRONLY | Write only |
O_RDWR | Read and write |
O_CREAT | Creates the file if it doesn’t exist (needs the third parameter, the permissions) |
O_TRUNC | If it already existed, empties it (size 0) when opening |
O_APPEND | Every write goes to the end of the file, overwriting nothing |
O_EXCL | Together with O_CREAT: fails if the file already exists |
Why |? Each flag is a number with a single bit set, and OR combines them into a single int: O_WRONLY | O_CREAT | O_TRUNC means “write, create it if needed and empty it”. Now see how the most important ones behave:
#include <stdio.h>
#include <fcntl.h> /* open and the O_... constants */
#include <unistd.h> /* read, write, lseek, close */
#include <sys/stat.h> /* stat and the permission bits */
/* Reads the whole file and shows it with its size */
static void show(const char *name) {
char buf[128];
int fd = open(name, O_RDONLY);
ssize_t n = read(fd, buf, sizeof buf - 1);
close(fd);
buf[n] = '\0';
printf(" (%zd bytes)\n%s", n, buf);
}
int main(void) {
/* O_TRUNC: if it already existed, it is emptied before writing */
int fd = open("saves.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, "Lia level 03 0450\n", 20);
close(fd);
printf("1) After O_TRUNC:");
show("saves.sav");
/* O_APPEND: every write goes to the end, overwriting nothing */
fd = open("saves.sav", O_WRONLY | O_APPEND);
write(fd, "Max level 12 2750\n", 20);
close(fd);
printf("2) After O_APPEND:");
show("saves.sav");
/* O_CREAT | O_EXCL: fails if the file already exists */
fd = open("saves.sav", O_WRONLY | O_CREAT | O_EXCL, 0644);
printf("3) O_CREAT | O_EXCL returns %d\n", fd);
if (fd == -1)
perror(" saves.sav");
/* creat(name, mode) = open(name, O_WRONLY | O_CREAT | O_TRUNC, mode) */
fd = creat("copy.sav", 0644);
printf("4) creat returns fd %d\n", fd);
close(fd);
/* The permissions we asked for with 0644 */
struct stat st;
stat("saves.sav", &st);
printf("5) Permissions of saves.sav: %o\n", st.st_mode & 0777);
return 0;
}
Output:
1) After O_TRUNC: (20 bytes) Lia level 03 0450 2) After O_APPEND: (40 bytes) Lia level 03 0450 Max level 12 2750 3) O_CREAT | O_EXCL returns -1 saves.sav: File exists 4) creat returns fd 3 5) Permissions of saves.sav: 644
Between the lines:
- 1) With
O_TRUNCthe file starts empty and ends up with Lia’s 20 bytes. It’s the “save game” combination: the one I was missing in the opening story. - 2) With
O_APPEND, Max’swritegoes after it: 20 + 20 = 40 bytes. Nothing had to be moved; the kernel takes the pointer to the end before every write. - 3)
O_CREAT | O_EXCLmeans “create it, but only if it’s new”. Sincesaves.savalready existed,openreturns −1 andperrorsaysFile exists. It’s useful so you don’t overwrite a saved game by accident. - 4)
creat(path, mode)is an old call that is exactly equivalent toopen(path, O_WRONLY | O_CREAT | O_TRUNC, mode). It returns 3 because the previous ones were already closed insideshow. - 5) The permissions come out as
644, the ones we asked for with0644.
The third parameter: permissions in octal
0644 starts with 0 because it’s in octal (base 8): each digit is 3 bits, one per permission (read = 4, write = 2, execute = 1).
| Digit | For whom | Sum | Permissions |
|---|---|---|---|
| 6 | owner | 4 + 2 | rw- |
| 4 | group | 4 | r-- |
| 4 | others | 4 | r-- |
So 0644 is rw-r--r--, the same you’d see with ls -l. Careful: without the leading 0, 644 would be a decimal number (1204 in octal) and the permissions would come out really strange.
Using O_CREAT without the third parameter. The compiler doesn’t complain (not even with -Wall), but open takes as permissions whatever happens to be in memory at that moment: sometimes you get a file not even you can read. With O_CREAT, always the mode.
The system also applies the user’s umask, which removes permissions. The usual one is 022 (removes write permission for group and others), which is why 0644 arrives intact. If you asked for 0666, with that umask you’d still get 644.
read: reading bytes (and what read does NOT do)
ssize_t read(int fd, void *buf, size_t count);
read tries to read count bytes from descriptor fd and copies them into buf, starting wherever the pointer is. It returns:
- the number of bytes it actually read (it can be fewer than you asked for);
- 0 if the pointer was already at the end of the file;
- −1 if there’s an error.
And after reading, the pointer moves forward by those bytes. That’s why you read in a loop until it returns 0:
#include <stdio.h>
#include <fcntl.h> /* open and the O_... constants */
#include <unistd.h> /* read, write, lseek, close */
int main(void) {
int fd = open("save.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, "Lia level 03 0450\n", 20);
close(fd);
fd = open("save.sav", O_RDONLY);
if (fd == -1) { /* always check for -1 */
perror("save.sav");
return 1;
}
char buf[9]; /* 8 bytes + room for the '\0' */
ssize_t nread;
int round = 1;
/* read returns the bytes read; 0 = end of file */
while ((nread = read(fd, buf, 8)) > 0) {
buf[nread] = '\0'; /* I add it: read doesn't */
printf("round %d: read returns %zd -> \"", round, nread);
for (char *p = buf; *p != '\0'; p++) /* I walk up to the '\0' */
if (*p == '\n')
printf("\\n"); /* the newline is a byte too */
else
putchar(*p);
printf("\"\n");
round++;
}
printf("round %d: read returns %zd (end of file)\n", round, nread);
close(fd);
return 0;
}
The output, round by round:
round 1: read returns 8 -> "Lia le" round 2: read returns 8 -> "vel 03 0" round 3: read returns 4 -> "450\n" round 4: read returns 0 (end of file)
| Round | Pointer before | read returns | Bytes read | Pointer after |
|---|---|---|---|---|
| 1 | 0 | 8 | Lia le | 8 |
| 2 | 8 | 8 | vel 03 0 | 16 |
| 3 | 16 | 4 | 450\n | 20 |
| 4 | 20 | 0 | (nothing: end of file) | 20 |
Check: 8 + 8 + 4 = 20 bytes, the size of the slot. In round 3 we asked for 8 but only 4 were left: read doesn’t make up the other 4, it simply returns fewer. And the \n counts as one more byte.
You add the ‘\0’ yourself
Look at the line buf[nread] = '\0';. read copies bytes; it knows nothing about C strings, so it doesn’t add the null character. If you then use buf as a string (%s, strlen, or a loop up to the '\0' as here), you have to terminate it yourself. Two details make it work:
- The buffer is 9 bytes long, one more than the 8 we ask for, so the
'\0'always fits. - It goes in
buf[nread], not inbuf[8]: in round 3 only 4 bytes were read, and what’s in positions 4-7 are leftovers from round 2.
printf("%s", buf) right after read, without the '\0'. Sometimes it works (if there happened to be a zero in memory) and sometimes it prints garbage or leftovers from the previous read. You’ll see it live in the exercises.
write and close
ssize_t write(int fd, const void *buf, size_t count); int close(int fd);
write writes count bytes from buf at the pointer’s position (or at the end, with O_APPEND), moves the pointer forward and returns how many it wrote, or −1. If the pointer is in the middle of the file, it overwrites: it doesn’t insert, it doesn’t shift what comes after. That’s exactly what happened to my saved game.
Two rules for the third argument:
- To write a string,
strlen(text), notsizeof(text): iftextis achar *,sizeofis 8 (the size of a pointer), not the length. - To write what you’ve just read, the value
readreturned (nread), not the buffer size. In the practice we’ll copy a file like that.
close(fd) frees the descriptor (the number becomes free again, as you saw with 4) and the open-file entry if nobody else is using it. When a process ends, the kernel closes all its descriptors, but closing them by hand is the right thing to do: a process has a maximum number of open descriptors, and a long-running program that opens without closing ends up running out of them.
lseek: moving the pointer
Every open file has its pointer, which marks where the next read or write will happen. lseek moves it without reading or writing anything:
off_t lseek(int fd, off_t offset, int whence);
The third argument says where the offset (which can be negative) is counted from:
whence | Counts from | Example with an 80-byte file |
|---|---|---|
SEEK_SET | the start (byte 0) | lseek(fd, 40, SEEK_SET) → 40 |
SEEK_CUR | the current position | if you’re at 43, lseek(fd, 9, SEEK_CUR) → 52 |
SEEK_END | the end (the size) | lseek(fd, -2, SEEK_END) → 78 |
It returns the new position (or −1). Two widely used tricks come from that: lseek(fd, 0, SEEK_CUR) tells you where you are without moving, and lseek(fd, 0, SEEK_END) tells you how big the file is.
Let’s try it with a file of four saved games. Each one takes exactly 20 bytes (we’ll call it a slot), so slot 2 starts at byte 2 · 20 = 40:
#include <stdio.h>
#include <fcntl.h> /* open and the O_... constants */
#include <unistd.h> /* read, write, lseek, close */
#define SLOT_SIZE 20
#define SAVEDATA "Lia level 03 0450\n" "Max level 12 2750\n" \
"Ane level 05 0980\n" "Teo level 07 1200\n"
int main(void) {
int fd = open("saves.sav", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("saves.sav");
return 1;
}
write(fd, SAVEDATA, 4 * SLOT_SIZE); /* 80 bytes */
printf("After writing, the pointer is at %ld\n", (long) lseek(fd, 0, SEEK_CUR));
/* SEEK_SET: from the start. Slot 2 = byte 2 * 20 = 40 */
char name[4];
off_t pos = lseek(fd, 2 * SLOT_SIZE, SEEK_SET);
read(fd, name, 3);
name[3] = '\0';
printf("SEEK_SET 40 -> %ld, leo \"%s\"\n", (long) pos, name);
printf("After reading 3 bytes, SEEK_CUR 0 -> %ld\n", (long) lseek(fd, 0, SEEK_CUR));
/* SEEK_CUR: from where I am. 43 + 9 = 52, where the level starts */
char level[3];
pos = lseek(fd, 9, SEEK_CUR);
read(fd, level, 2);
level[2] = '\0';
printf("SEEK_CUR +9 -> %ld, Ane's level: %s\n", (long) pos, level);
/* SEEK_END: from the end. The last byte (79) is the '\n' */
char c;
pos = lseek(fd, -2, SEEK_END); /* 80 - 2 = 78 */
read(fd, &c, 1);
printf("SEEK_END -2 -> %ld, last digit: %c\n", (long) pos, c);
printf("SEEK_END 0 -> %ld = file size\n", (long) lseek(fd, 0, SEEK_END));
/* Past the end: the gap is filled with zeros */
pos = lseek(fd, 5, SEEK_END); /* 80 + 5 = 85 */
write(fd, "!", 1);
printf("SEEK_END +5 -> %ld; after writing 1 byte, size %ld\n",
(long) pos, (long) lseek(fd, 0, SEEK_END));
unsigned char tail[6];
lseek(fd, 80, SEEK_SET);
read(fd, tail, 6);
printf("Bytes 80 to 85:");
for (int i = 0; i < 6; i++)
printf(" %d", tail[i]); /* 33 is the code of '!' */
printf("\n");
close(fd);
return 0;
}
Output:
After writing, the pointer is at 80 SEEK_SET 40 -> 40, read "Ane" After reading 3 bytes, SEEK_CUR 0 -> 43 SEEK_CUR +9 -> 52, Ane's level: 05 SEEK_END -2 -> 78, last digit: 0 SEEK_END 0 -> 80 = file size SEEK_END +5 -> 85; after writing 1 byte, size 86 Bytes 80 to 85: 0 0 0 0 0 33
Follow the arrows in the diagram with the output in front of you:
- After writing the 80 bytes, the pointer is at the end: 80.
SEEK_SET 40jumps to slot 2 and a 3-bytereadreadsAne. The pointer moves on: 40 + 3 = 43.SEEK_CUR +9counts from 43: 43 + 9 = 52, exactly where Ane’s level (05) is.SEEK_END -2counts from the size: 80 − 2 = 78. Why −2 and not −1? Because the last byte (79) is the\n. The last digit of Teo’s points (1200) is one before it.SEEK_END 0returns 80: the size.SEEK_END +5takes the pointer past the end, to 85. That’s not an error. When the!is written there, the file grows to 86 bytes and bytes 80 to 84, which we never wrote, are 0 (33 is the ASCII code of!). That’s the “gap of zeros”.
lseek neither reads nor writes: it only changes a number in the open-file entry. That’s why you can take the pointer outside the file without an error; the problem (or the fun) comes when you write.
lseek returns off_t, an integer that can be 64 bits. To print it with printf it’s cast to long and printed with %ld, as in the code.
Descriptors inherited with fork
As promised in the three-tables diagram. Now that you know the pointer lives in the open-file entry, what do you think happens in these two cases?
- A) A process opens the same file twice, writes Lia with the first descriptor and Max with the second.
- B) A process opens the file once, calls
fork, the child writes Lia and, when it finishes, the parent writes Max.
#include <stdio.h>
#include <fcntl.h> /* open and the O_... constants */
#include <unistd.h> /* read, write, lseek, close */
#include <sys/types.h> /* pid_t */
#include <sys/wait.h> /* waitpid */
/* Reads the whole file and shows it with its size */
static void show(const char *name) {
char buf[128];
int fd = open(name, O_RDONLY);
ssize_t n = read(fd, buf, sizeof buf - 1);
close(fd);
buf[n] = '\0';
printf(" (%zd bytes)\n%s", n, buf);
}
int main(void) {
/* A) Two opens of the same file: two independent pointers */
int a = open("two_opens.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
int b = open("two_opens.sav", O_WRONLY);
write(a, "Lia level 03 0450\n", 20); /* a's pointer moves to 20 */
write(b, "Max level 12 2750\n", 20); /* b's is still at 0: overwrites Lia */
close(a);
close(b);
printf("A) Two opens:");
show("two_opens.sav");
/* B) A single open and then fork: parent and child share the pointer */
int fd = open("fork.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
pid_t pid = fork();
if (pid == 0) { /* child: inherits fd 3 */
write(fd, "Lia level 03 0450\n", 20);
close(fd); /* closes ITS copy of the fd */
return 0;
}
waitpid(pid, NULL, 0); /* the parent waits for the child */
printf("B) The parent sees the pointer at %ld\n", (long) lseek(fd, 0, SEEK_CUR));
write(fd, "Max level 12 2750\n", 20); /* writes after it, not on top */
close(fd);
printf("B) open + fork:");
show("fork.sav");
return 0;
}
Output:
A) Two opens: (20 bytes) Max level 12 2750 B) The parent sees the pointer at 20 B) open + fork: (40 bytes) Lia level 03 0450 Max level 12 2750
- In A, each
opencreated its own entry with its own pointer.a‘s moved to 20, butb‘s was still at 0, so Max overwrote Lia: 20 bytes and Lia gone. Another way to lose a saved game. - In B, the child inherited descriptor 3, which points to the same entry as the parent’s. When the child wrote its 20 bytes, the shared pointer moved to 20, and the parent sees it that way (
The parent sees the pointer at 20). Itswritegoes after: 40 bytes, nothing overwritten.
The waitpid from the processes article is what guarantees the order: without it, parent and child could write at the same time. And each one does its own close: the child closes its copy of the descriptor, and the entry stays alive until the parent closes its own.
Common mistakes
Opening for writing without O_TRUNC when you want to replace the contents. If the new data is shorter, leftovers of the old data remain (Max’s saved game).
O_CREAT without the permissions, or with 644 instead of 0644.
Using the buffer as a string without buf[nread] = '\0', or reserving exactly the bytes you ask read for and leaving no room for the '\0'.
Writing sizeof buf bytes instead of the nread ones: you copy garbage from the end of the buffer.
lseek(fd, -1, SEEK_END) to read the last character of a text file: the last byte is usually the \n.
Opening with O_WRONLY and then trying to read (or the other way round). read returns −1 with Bad file descriptor; if you’re going to do everything with the same descriptor, O_RDWR.
Watch it step by step in Python Tutor
Python Tutor runs code line by line and draws the variables and memory; choose C in the drop-down. It isn’t designed to work with real files on disk, so here we simulate one: a struct with the bytes, the size and the pointer. my_lseek and my_read do the same as the real ones, so you can watch the pointer move at every step:
#include <stdio.h>
#define SET 0
#define CUR 1
#define END 2
struct File { char data[16]; int size; int pos; }; /* pos = the pointer */
int my_lseek(struct File *f, int off, int whence) {
int base = whence == SET ? 0 : whence == CUR ? f->pos : f->size;
f->pos = base + off; /* only the pointer moves */
return f->pos;
}
int my_read(struct File *f, char *buf, int n) {
int i = 0;
while (i < n && f->pos < f->size)
buf[i++] = f->data[f->pos++]; /* copies one byte and moves on */
return i; /* 0 = end of file */
}
int main(void) {
struct File f = {"Lia 03\nMax 12\n", 14, 0};
char buf[8] = {0};
int n = my_read(&f, buf, 3); /* "Lia": pos becomes 3 */
my_lseek(&f, 4, CUR); /* skips " 03\n": pos 7 */
n = my_read(&f, buf, 3); /* "Max" */
int p = my_lseek(&f, -3, END); /* 14 - 3 = 11 */
n = my_read(&f, buf, 8); /* I ask for 8, only 3 are left */
printf("%d %d %c%c\n", p, n, buf[0], buf[1]);
return 0;
}
When you run it, the last line it prints is:
11 3 12
What to watch:
- In the first
my_read, see howf.posgoes from 0 to 3 whilebuffills up withL,i,a: reading moves the pointer. - In
my_lseek(&f, 4, CUR), onlyf.poschanges (from 3 to 7).dataandbufaren’t touched: that’s alllseekdoes. - In
my_lseek(&f, -3, END), the base isf->size(14), and 14 − 3 = 11, the byte of the1in12. - In the last
my_read, 8 bytes are requested, but thewhilestops whenposreachessize: it returns 3, like the realreadin the last round.
Summary
| Call | What it does | Returns | Watch out for |
|---|---|---|---|
open(path, flags[, mode]) | Opens or creates and gives a descriptor | The lowest free descriptor, or −1 | O_TRUNC, and 0644 with O_CREAT |
read(fd, buf, n) | Reads up to n bytes and moves on | Bytes read, 0 at the end, −1 | It doesn’t add the '\0' |
write(fd, buf, n) | Writes n bytes and moves on | Bytes written or −1 | It overwrites; strlen or nread |
lseek(fd, offset, whence) | Moves the pointer | The new position or −1 | SEEK_END points past the last byte |
close(fd) | Frees the descriptor | 0 or −1 | Always close what you open |
Three ideas to take away:
- A descriptor is just a number (0, 1 and 2 come built in); the pointer and the mode live in the open-file entry, and two
opens are two pointers, but aforkshares the same one. readandwritework with bytes and tell you how many they moved: use that number, and add the'\0'yourself if you’re going to treat what you read as text.- With
lseekand fixed-size records you can go straight to any field: position = record size · number + offset within the record.
In the practice we build the complete saves file: we save it slot by slot, make a backup with a read and write loop and edit the points of one saved game without touching the rest. After that, on Linux, comes another piece you’ve already glimpsed with 0644: permissions and the chmod command.
Did this article help you? Here is how you can support SergioLearns →
