Saturday, November 1, 2014

Unix Prog: Threads and fork

1. Process Inheritance

When a thread calls "fork", a copy of the entire process address space is made for the child. By inheriting a copy of the address space, the child also inherits the state of every mutex, reader-writer lock, and condition variable from the parent process. If the parent consists of more than one thread, the child will need to clean up the lock state if it isn't going to call exec immediately after fork returns.

Inside the child process, only one thread exists, which is a copy of the thread that called fork in the parent. If locks are held in parent process, they are still held at child process, but since child only has one thread, there is no way to know who hold the lock. So we need to clean up the lock state while doing the fork.

System Definition:
 ubuntu@ip-172-31-23-227:~$ less /usr/include/pthread.h  
 ......  
 /* Install handlers to be called when a new process is created with FORK.  
   The PREPARE handler is called in the parent process just before performing  
   FORK. The PARENT handler is called in the parent process just after FORK.  
   The CHILD handler is called in the child process. Each of the three  
   handlers can be NULL, meaning that no handler needs to be called at that  
   point.  
   PTHREAD_ATFORK can be called several times, in which case the PREPARE  
   handlers are called in LIFO order (last added with PTHREAD_ATFORK,  
   first called before FORK), and the PARENT and CHILD handlers are called  
   in FIFO (first added, first called). */  
   
 extern int pthread_atfork (void (*__prepare) (void),  
               void (*__parent) (void),  
               void (*__child) (void)) __THROW;  
 ......  

Before doing the fork, prepare is called to lock all mutexes. After doing the fork we have 2 copies of locked mutexes in both parent and child process. "Parent" handler release the lock at parent process, and "child" handler release the lock at child process.

2. Example:
atfork.c:
 #include<stdio.h>  
 #include<stdlib.h>  
 #include<unistd.h>  
 #include<pthread.h>  
   
 pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;  
 pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;  
   
 void prepare(void)  
 {  
  // Acquire all locks in this handler, but this handler aims to  
  // wait for these mutexes getting unlocked firstly, otherwise  
  // it will block here forever  
  printf("preparing locks...\n");  
  pthread_mutex_lock(&lock1);  
  pthread_mutex_lock(&lock2);  
 }  
   
 void parent(void)  
 {  
  printf("parent unlocking locks...\n");  
  pthread_mutex_unlock(&lock1);  
  pthread_mutex_unlock(&lock2);  
 }  
   
 void child(void)  
 {  
  printf("child unlocking locks...\n");  
  pthread_mutex_unlock(&lock1);  
  pthread_mutex_unlock(&lock2);  
 }  
   
 void* thr_fn(void* arg)  
 {  
  printf("thread started...\n");  
  pthread_mutex_lock(&lock1);  
  pthread_mutex_lock(&lock2);  
   
  sleep(2);  
   
  // We have to unlock the mutex, otherwise, prepare handler will be  
  // blocked forever.  
  pthread_mutex_unlock(&lock1);  
  pthread_mutex_unlock(&lock2);  
  return 0;  
 }  
   
 int main(int argc, char* argv[])  
 {  
  int err;  
  pid_t pid;  
  pthread_t tid;  
   
  // Establish the at fork handlers  
  if((err = pthread_atfork(prepare, parent, child)) != 0) {  
   printf("pthread_atfork error!\n");  
   exit(1);  
  }  
   
  // Launch the new thread  
  if((err = pthread_create(&tid, NULL, thr_fn, 0)) != 0) {  
   printf("pthread_create error!\n");  
   exit(2);  
  }  
   
  // fork the child process  
  sleep(1);  
  printf("parent about to fork...\n");  
   
  if((pid = fork()) < 0) {  
   printf("fork failed!\n");  
  }  
  else if(pid == 0) /* child */ {  
   printf("child returned from fork\n");  
  }  
  else {  
   printf("parent returned from fork\n");  
  }  
   
  exit(0);  
 }  

shell:
 ubuntu@ip-172-31-23-227:~$ ./atfork.out  
 thread started...  
 parent about to fork...  
 preparing locks...  
 parent unlocking locks...  
 parent returned from fork  
 ubuntu@ip-172-31-23-227:~$ child unlocking locks...  
 child returned from fork  

3. Threads I/O

System Definition:
 ubuntu@ip-172-31-23-227:~$ less /usr/include/unistd.h  
 ......  
 /* Read NBYTES into BUF from FD at the given position OFFSET without  
   changing the file pointer. Return the number read, -1 for errors  
   or 0 for EOF.  
   
   This function is a cancellation point and therefore not marked with  
   __THROW. */  
 extern ssize_t pread (int __fd, void *__buf, size_t __nbytes,  
            __off_t __offset) __wur;  
   
 /* Write N bytes of BUF to FD at the given position OFFSET without  
   changing the file pointer. Return the number written, or -1.  
   
   This function is a cancellation point and therefore not marked with  
   __THROW. */  
 extern ssize_t pwrite (int __fd, const void *__buf, size_t __n,  
             __off_t __offset) __wur;  
 ......  

pread is equal to "lseek" + "read"
pwrite is equal to "lseek" + "write"

By combining two function together to be atomic, it can help solve many I/O synchronization problem among threads.

No comments:

Post a Comment