0

I want to write log files using mmap in multi-process server, the processes can write to the initial log file, but if one process modify the shared address to a new file, other processes can not perceive this. The codes are like this:

firstly: open a file and map it to shared memory;

secondly: child process writes something to the file

thirdly: child process open a new log and modify the shared memory to the new file

fourthly: parent process writes something to the log file. I want the parent process write to the new log, but it writes to the old file. This means the parent process does not know the modification of child process.

my question is : How can the parent(or other children) process perceive the modification of the shared memory, and write to the new file?

typedef struct shared_mgs_s {
    log_atomic_t lock;
    int          offset;    
} shared_msg_t;
char         *write_addr;

shared_msg_t    *sm;
log_atomic_t    *log_mutex_ptr;

#define FILE_SIZE 1 * 1024 * 1024

int map_log_file(char *file_name)
{
    // create a log file and map it to shared memory
    int fd = -1;
    if ((fd = open(file_name, O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR)) < 0)
    {
        printf("create %s failed %d\n",file_name, errno);
        return -1;
    }
    
    if (ftruncate(fd, FILE_SIZE) < 0)
    {
        printf("ftruncate %d failed", errno);
        return -1;
    }
    write_addr = (char*)mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
    sm->offset = 0;
    
    return 0;
}

void write_log(char *msg)
{    
    memcpy(write_addr + sm->offset, msg, strlen(msg));
    sm->offset += strlen(msg);
}

int create_shared_info()
{
    sm = mmap(NULL, 64, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (!sm) 
        return -1;

    sm->offset = 0;
    sm->lock = 0;
    log_mutex_ptr = &(sm->lock);
}

int main()
{
    if(create_shared_info() < 0)
    {
        printf("create shared info failed\n");
        return -1;
    }

    if(map_log_file("./old.log") != 0)
    {
        printf("map old log failed\n");
        return -1;
    }
    if(0 == fork())
    {
        log_spinlock(log_mutex_ptr, 1, 2048);
        munmap(write_addr, FILE_SIZE);
        if(map_log_file("./new.log") != 0)
        {
            printf("create new log failed\n");
            return -1;
        }
        write_log("from child\n");
        log_unlock(log_mutex_ptr);
        exit(0);
    } else {
        usleep(10); // make sure child process get lock firstly, just test use the new file
        log_spinlock(log_mutex_ptr, 1, 2048);
        write_log("from parent\n");
        log_unlock(log_mutex_ptr);
    }

    return 0;
}

in the way, the parent writes to old.log, this is not what I want, I want it write to the new one.

Klen
  • 13
  • 4
  • As you're finding out, that's a very fragile and error prone methodology. Just use normal i/o functions to write to the log file. If one process is creating new files instead of truncating an existing one, you'll want to open the file each time, write a log message, and close it. – Shawn May 11 '23 at 06:01
  • 1
    OT: What books, tutorials or classes have taught you to define [the `main` function](https://en.cppreference.com/w/c/language/main_function) in that way? – Some programmer dude May 11 '23 at 06:03
  • @Shawn normal i/o is too slowly for us, this is why we use `mmap` – Klen May 11 '23 at 06:04
  • @Someprogrammerdude This is just a small sample written to illustrate the problem – Klen May 11 '23 at 06:05
  • So the child process mapped a new file to a new address, and send that address to the parent. But for the parent that address makes no sense, it is an address from the child address space which is now unrelated to the parent's address space, and the parent definitely doesn't have it mapped to the new file. – n. m. could be an AI May 11 '23 at 06:12
  • 1
    @KlenZhang Instead of pseudo-code, prefer making your small samples into [mre]s. – Ted Lyngmo May 11 '23 at 06:20
  • Creating simple or small examples is still no excuse for bad code. And on that note, `void *` pointers can be implicitly converted to any other kind of pointer, except pointers to functions. Therefore there's no need to cast such pointer. In fact, casting such pointer could lead to problems if you forget to include the correct header files. It's the same issue as [casting the result of `malloc`](https://stackoverflow.com/questions/605845/do-i-cast-the-result-of-malloc). – Some programmer dude May 11 '23 at 06:31
  • 1
    On a note more related to your problem, when you "make sure child process get lock firstly..." that's not really enough. If you want synchronization between processes, you need to do it explicitly. Semaphores, mutexes, message queues, etc. – Some programmer dude May 11 '23 at 06:35
  • 1
    Note that all identifiers that end with `_t` are [reserved by POSIX](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/functions/V2_chap02.html#tag_15_02_02). Using something like `shared_msg_t` is likely to cause a collision and undefined behavior. And if `atomic_t` is not the Linux-provided one, you've already done that. – Andrew Henle May 11 '23 at 07:48
  • Is there a reason you want to let each process write to the log file on their own? Why not have a single process whose only responsibility is to do the logging, then all other processes can use message queues to communicate with the logger process. All your synchronization problems would be solved that way, since you don't need any synchronization. :) – Some programmer dude May 11 '23 at 08:18
  • @Someprogrammerdude Yes, this is an option that we are evaluating. We want to transform an old multi-process service, and the transformation should be as small as possible. If a process is created to record logs, it will involve the problem of log process management, such as it dies, and so on. – Klen May 11 '23 at 08:39
  • @Someprogrammerdude I have modified `main`, I hope this will look better – Klen May 11 '23 at 08:41
  • @KlenZhang A [mre] is something that we can compile (_as-is_) and that will preproduce the problem. Even though you've modified `main`, your example can't be compiled. – Ted Lyngmo May 11 '23 at 10:19
  • 1
    @Someprogrammerdude *then all other processes can use message queues to communicate with the logger process.* If you can guarantee the size of each log message is less than or equal to `PIPE_BUF` bytes, then you can replace message queues with a single named pipe. Then the logging functionality in each process would have to use a single `write()` call to write a log entry atomically to the pipe. – Andrew Henle May 11 '23 at 21:02

1 Answers1

0

but if one process modify the shared address to a new file, other processes can not perceive this.

Another way to address this:

  1. Let another process change the log file name with something like the mv command-line utility or the rename() POSIX function. For example, mv /log/file.log /log/file.log.1. As you noticed, this will mean all the processes that were logging to /log/file.log will now be logging to /log/file.log.1, which you don't want.

  2. Each process using your logging functionality will have a signal handler (historically, Unix processes have used SIGHUP) that will cause it to reopen its log file, thus picking up the new log file with a simple open() call. If your log functionality is fully formatting each log message internally and using a single write() call with the log file opened with the O_APPEND flag set, even in a multithreaded process you can replace the active log file descriptor atomically with open( "/log/file.log", O_WRITE | O_CREAT | O_APPEND, ...) to a temporary file descriptor and then you replace the log file descriptor with a call to dup2(): dup2( tempLogFD, logFileFD ); And since both open() and dup2() are async-signal-safe, you can do both calls in your SIGHUP signal handler:

    // needs headers and error-checking
    int logFileFD;
    
    void sighupHandler( int sig )
    {
        int tempLogFD = open( "/log/file.log", O_WRONLY | O_CREAT | O_APPEND,
            0644 );
        dup2( tempLogFD, logFileFD );
        close( tempLogFD );
    }
    
  3. You can use the Linux command fuser -SIGHUP /log/file.log.1 (the renamed original log file) to send SIGHUP to all the processes that are still writing to /log/file.log.1 and they will swap writing their logs from /log/file.log.1 to the new /log/file.log.

Note that there's no complex synchronization nor any shared resources needed between your processes.

Andrew Henle
  • 32,625
  • 3
  • 24
  • 56