1

I have a very simple C socket server: the main process is listening on a specific port. When a new request arrives, fork() is called: the child process enters a function called dosomething(), returns a response to the client, logs everything in a text file, then dies. This is a simplified view:

void dosomething(int socketFd)
{
    /* ... */
    //Reads the request and sends a response
    writetolog("[INFO] Request accepted. Sent response message -> ", buffer);
    /* ... */
}

This is the logging function:

void writetolog(char* logString1, char* logString2)
{
    /* ... */
    //Prepends date and time, and formats everything nicely into a single char[]
    if ((logFd = open("SocketServer.log", O_CREAT | O_WRONLY | O_APPEND, 0644)) >= 0) {
        write(logFd, logBuffer, strlen(logBuffer));
        close(logFd);
    }
}

Now, my question: since this server is (theoretically) able to process multiple requests simultaneously (so, multiple process may want to write something in the log), do I have to introduce any synchronization (locking) logic for the log file?

Given that the "critical section" is that single write() call: can multiple write() calls on the same file descriptor be "mixed" because of the OS scheduling? Is that a real risk? For example:

Process1 wants to write "ABC"
Process2 wants to write "123"
Result: "AB1C23"

I tried sending thousands of requests from three different clients in the same time window. The log file was correctly written every single time, no "mixing" at all. Can I conclude that the write() system call is atomic, at least in POSIX-compliant systems?

Bonus question: let's say that the logging function uses two write() calls instead of one. The synchronization mechanism should not be optional anymore, because I want to make sure the two calls are executed without being interrupted by another process. What's the simplest locking object I should use in this case? Mutex would be enough?

kleewan
  • 33
  • 1
  • 4
  • The `write` system call does not guarantee to write all of the requested bytes. The return value tells you how many bytes it *actually* wrote (which will be at most the number you specified). I don't know all of the conditions under which it may be smaller, but it can happen if the output device runs out of space. This doesn't really answer your question, but it's worth knowing. – Tom Karzes Mar 05 '19 at 15:12
  • My expectation (which may or may not be fully justifiable) is that as long as each process makes a single write call to add the message, there shouldn't be issues with interleaving. The `O_APPEND` flag is important. You might want to consider whether it is better to open the file once (before forking) and then have the children use the same open file description (but each will have its own open file descriptor — go reread the [`read()`](http://pubs.opengroup.org/onlinepubs/9699919799/functions/read.html) specification if you need to refresh your memory on the difference). – Jonathan Leffler Mar 17 '19 at 23:03
  • You might find some useful information in [Detecting that a log file has been deleted or truncated on POSIX systems](https://stackoverflow.com/questions/462122/detecting-that-log-file-has-been-deleted-or-truncated-on-posix-systems) – Jonathan Leffler Mar 17 '19 at 23:05

1 Answers1

1

Q: "Do I have to introduce any synchronization (locking) logic for the log file?"

A: Yes. Writing simultaneously to the same file can produce race conditions and undesired behaviour.

Q: "Given that the "critical section" is that single write() call: can multiple write() calls on the same file descriptor be "mixed" because of the OS scheduling? Is that a real risk?"

A: Yes it is, and your example can happen.

To improve your code, open the log file once, and keep track of the file descriptor. Use a mutex lock inside writetolog.

I wrote a new version of writetolog with multiple parameter support (like printf):

Check Share condition variable & mutex between processes: does mutex have to locked before? for pthread_mutex_t _mutex_log_file initialization

#MAX_LEN_LOG_ENTRY 1024

// _log_fd is a file descriptor previously opened

void writetolog (char *fmt, ...)
{
    va_list ap;
    char   msg[MAX_LEN_LOG_ENTRY];

    va_start(ap, fmt);
    vsnprintf(msg, MAX_LEN_LOG_ENTRY - 1, fmt, ap);
    va_end(ap);

    pthread_mutex_lock (&_mutex_log_file);

    fprintf (_log_fd, "[ LOG ] %s\n", msg);
    fflush(_log_fd);

    pthread_mutex_unlock (&_mutex_log_file);
}

An example call of writetolog:

writetolog("Testing log function: %s %s %s", "hello", "world", "good");
  • 1
    Since fork is used here, the lock is across processes. The usage of mutex in the following link may be more comprehensive in this case. https://stackoverflow.com/questions/20325146/share-condition-variable-mutex-between-processes-does-mutex-have-to-locked-be – KL-Yang Mar 06 '19 at 01:09
  • That's very interesting, thank you all. So, in order for the mutex to be shared across all processes, do I have to declare it outside of the main() function (and initialize it before forking) ? – kleewan Mar 06 '19 at 12:00
  • @kleewan you can declare it in the main of the father process or outside it (global) but the initialization should be before forking (in the father process). – Horacio Goetendia Mar 06 '19 at 20:43
  • @HoracioGoetendia Mutex will not be shared for a child process, so this won't work. It has to be in a shared memory. –  Mar 06 '19 at 20:55