2

I am trying to simulate race conditions in writing to a file. This is what I am doing.

  1. Opening a.txt in append mode in process1
  2. writing "hello world" in process1
  3. prints the ftell in process1 which is 11
  4. put process1 in sleep
  5. open a.txt again in append mode in process2
  6. writing "hello world" in process2 (this correctly appends to the end of the file)
  7. prints the ftell in process2 which is 22 (correct)
  8. writing "bye world" in process2 (this correctly appends to the end of the file).
  9. process2 quits
  10. process1 resumes, and prints its ftell value, which is 11.
  11. writing "bye world" by process1 --- i assume as the ftell of process1 is 11, this should overwrite the file.

However, the write of process1 is writing to the end of the file and there is no contention in writing between the processes.

I am using fopen as fopen("./a.txt", "a+)

Can anyone tell why is this behavior and how can I simulate the race condition in writing to the file?

The code of process1:

#include <iostream>
#include <fstream>
#include <string>
#include <stdio.h>
#include "time.h"
using namespace std;
int main()
{

    FILE *f1= fopen("./a.txt","a+");
    cout<<"opened file1"<<endl;
    string data ("hello world");
    fwrite(data.c_str(), sizeof(char), data.size(),  f1);
    fflush(f1);
    cout<<"file1 tell "<<ftell(f1)<<endl;
    cout<<"wrote file1"<<endl;
    sleep(3);
    string data1 ("bye world");;
    cout<<"wrote file1 end"<<endl;
    cout<<"file1 2nd tell "<<ftell(f1)<<endl;
    fwrite(data1.c_str(), sizeof(char),  data1.size(),  f1);
    cout<<"file1 2nd tell "<<ftell(f1)<<endl;
    fflush(f1);
    return 0;
}

In process2, I have commented out the sleep statement.

I am using the following script to run:

./process1 &
sleep 2
./process2 &

Thanks for your time.

WhozCraig
  • 65,258
  • 11
  • 75
  • 141
Bill
  • 5,263
  • 6
  • 35
  • 50
  • This is not a race condition. They write sequentially. – littleadv Dec 22 '12 at 01:16
  • I am assuming as ftell when process1 wakes up is 11, the file pointer f1 for process1 will try to write from there. How can I simulate race condition, where one process overwrite other's content? Thanks. – Bill Dec 22 '12 at 01:18
  • ftell may point somewhere in the read buffer, but when it comes to the actual file - you're serialized. – littleadv Dec 22 '12 at 01:28
  • how does fwrite know where to start writing the data? Isn't it stored in the File pointer of a process? If it is, process1's end_of_file is outdated (i.e. process2 has written beyond that point). So, when process1 resumes writing, I was hoping it will use its value and start writing right away. Will fwrite recalculate the EOF before it writes? – Bill Dec 22 '12 at 01:32
  • OS handles that. It may be a library function bug that `ftell` gives you incorrect result (you can research it if you want), but when you open a file as append, it appends. – littleadv Dec 22 '12 at 01:34
  • Write one char at a time and use fflush() between writes. – brian beuning Dec 22 '12 at 02:30

4 Answers4

2

Writing in append mode is an atomic operation. This is why it doesn't break.

Now... how to break it?

Try memory mapping the file and writing in the memory from the two processes. I'm pretty sure this will break it.

sqreept
  • 5,236
  • 3
  • 21
  • 26
  • Do you mean that there will be no race condition in writing files, if you open the file in append mode from different processes? This is hard to believe though. – Bill Dec 22 '12 at 01:28
  • This thread on SO seems to indicate that it's NOT atomic unless under specific circumstances (and only on Linux/Unix, not Windows):http://stackoverflow.com/questions/7552451/can-multiple-processes-append-to-a-file-using-fopen-without-any-concurrency-prob – Mats Petersson Dec 22 '12 at 01:29
  • Most Unix & Linux File Systems do support atomic appends. Writing to log files would be funny without it. It is known that NFS notably doesn't support them. And there's no general way you can find out if you're dealing with atomic appends support or not. – sqreept Dec 22 '12 at 01:43
2

The writer code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BLOCKSIZE 1000000

int main(int argc, char **argv)
{
    FILE *f = fopen("a.txt", "a+");
    char *block = malloc(BLOCKSIZE);

    if (argc < 2)
    {
    fprintf(stderr, "need argument\n");
    }
    memset(block, argv[1][0], BLOCKSIZE);
    for(int i = 0; i < 3000; i++)
    {
    fwrite(block, sizeof(char), BLOCKSIZE, f);
    }
    fclose(f);
}

The reader function:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BLOCKSIZE 1000000

int main(int argc, char **argv)
{
    FILE *f = fopen("a.txt", "r");
    int c;
    int oldc = 0;
    int rl = 0;

    while((c = fgetc(f)) != EOF)
    {
    if (c != oldc)
    {
        if (rl)
        {
        printf("Got %d of %c\n", rl, oldc);
        }
        oldc = c;
        rl = 0;
    }
    rl++;
    }

    fclose(f);
}

I ran ./writefile A & ./writefile B then ./readfile

I got this:

Got 1000999424 of A
Got 999424 of B
Got 999424 of A
Got 4096 of B
Got 4096 of A
Got 995328 of B
Got 995328 of A
Got 4096 of B
Got 4096 of A
Got 995328 of B
Got 995328 of A
Got 4096 of B
Got 4096 of A
Got 995328 of B
Got 995328 of A
Got 4096 of B
Got 4096 of A
Got 995328 of B
Got 995328 of A
Got 4096 of B
Got 4096 of A
Got 995328 of B
Got 995328 of A

As you can see, there are nice long runs of A and B, but they are not exactly 1000000 characters long, which is the size I wrote them. The whole file, after a trialrun with a smaller size in the first run is just short of 7GB.

For reference: Fedora Core 16, with my own compiled 3.7rc5 kernel, gcc 4.6.3, x86-64, and ext4 on top of lvm, AMD PhenomII quad core processor, 16GB of RAM

Mats Petersson
  • 126,704
  • 14
  • 140
  • 227
  • fflush doesn't seem to help. – Mats Petersson Dec 22 '12 at 02:15
  • in your reader code, you may want to print the values counted for the last iteration of the loop which was not printed. otherwise, if will not report the last N characters in the file which were identical. I added your printf line after the end of while loop and it reports all the characters in the file. – Bill Dec 22 '12 at 03:20
  • Thanks Bill. It does go wrong well before the last iteration, so it wasn't something I concerned myself with. And it's not like I claimed "this is perfect code" - in fact it's missing a lot of error handling... – Mats Petersson Dec 22 '12 at 20:18
1

I'm pretty sure you can't RELY on this behaviour, but it may well work reliably on some systems. Writing to the same file from two different processes is likely to cause problems sooner or later, if you "try hard enough". And sod's law says that that's exactly when your boss is checking if the software works, when your customer takes delivery of the system you've sold, or when you are finalizing your report that took ages to produce, or some other important time.

Mats Petersson
  • 126,704
  • 14
  • 140
  • 227
  • I am trying to break this thing, i.e. make them overwrite each other. Thanks for the laugh though. – Bill Dec 22 '12 at 01:21
  • Try wrting MUCH larger blocks (several megabytes, possibly), and really hammer out data. You can make it easy by writing large blocks of 'A' in the one process, and large blocks of 'b' in the other. oh, and don't use fflush! – Mats Petersson Dec 22 '12 at 01:24
  • 1
    Sounds like a challenge... ;) – Mats Petersson Dec 22 '12 at 01:43
  • 1
    Well, I've broken it... I wrote blocks of 1000000 characters in a loop, and then wrote a bit of code to analyse the output: Got 1000999424 of A Got 999424 of B Got 999424 of A Got 4096 of B Got 4096 of A Got 995328 of B Got 995328 of A Got 4096 of B Got 4096 of A Got 995328 of B Got 995328 of A Got 4096 of B Got 4096 of A Got 995328 of B Got 995328 of A Got 4096 of B That's the output of my analyser program. It's clearly not working. – Mats Petersson Dec 22 '12 at 01:59
  • If you don't mind, I'll add another answer, with code showing that it's broken... – Mats Petersson Dec 22 '12 at 02:00
1

The behavior you're trying to break or see depends on which OS you are working on, as writing in a file is a system call. On what you told us about the first file descriptor to not overwrite what the second process wrote, the fact you opened the file in append mode in both process may have actualized the ftell value before actually writing in it.

Did you try to do the same with the standard open and write functions? Might be interesting as well.

EDIT: The C++ Reference doc explains about the fopen append option here: "append/update: Open a file for update (both for input and output) with all output operations writing data at the end of the file. Repositioning operations (fseek, fsetpos, rewind) affects the next input operations, but output operations move the position back to the end of file." This explains the behavior you observed.

BenMorel
  • 34,448
  • 50
  • 182
  • 322
tsukasan
  • 118
  • 5
  • before process1 goes to sleep, its ftell is 11. when it wakes up, its value is still 11. however, in the mean time, process2 had written. so, I was hoping process1 will resume writing from 11, as that was its EOF before it went to sleep. If each fwrite recalculates the EOF (like open with O_APPEND flag does), then it explains the outcome. – Bill Dec 22 '12 at 01:38
  • You can easily try to see if it is the case by writing no characters in the file, with `fwrite("", sizeof(char), 0, f1)` and then checking again the ftell value for f1. – tsukasan Dec 22 '12 at 01:49
  • I found something usefull back in the C++ Reference doc: "append/update: Open a file for update (both for input and output) with all output operations writing data at the end of the file. Repositioning operations (fseek, fsetpos, rewind) affects the next input operations, but output operations move the position back to the end of file. The file is created if it does not exist." http://www.cplusplus.com/reference/cstdio/fopen/ I think that explains the fact that the first process will write at the end of file whatever happened before. I'll edit my answer to add those details. – tsukasan Dec 22 '12 at 01:59
  • So how can you explain the behavior observed by the code below given by Mats Petersson. It seems that the fwrite of two processes are intermixed. – Bill Dec 22 '12 at 03:22
  • It would be pretty hard to tell how exactly his code works because he is working on his own kernel based on a Fedora distribution, so he may have modified the way system calls work. But he doesn't seem to check the return value of fwrite, which could explain the behavior here, because fwrite return the number of chars it successfully wrote in the stream. – tsukasan Dec 22 '12 at 14:00
  • I checked the return value. the fwrire's are writing all characters at one shot to the stream. However, it is possible OS does not write each stream sequentially due to some process swapping or something. That may explain why the streams are getting intermixed. But, interesting thing is whenever the stream is fflushed, it is always at the end, they are not overwritten. BTW, I also added fflush after the fwrite to ensure everything is flushed right away. – Bill Dec 22 '12 at 14:54