posix多线程有感--线程高级编程(线程和fork,exec)
当多线程进程调用fork创建子进程时,Pthreads指定只有那个调用fork的线程在子进程内存在(表示子进程中只有调用线程这个线程)。尽管当从fork调用返回时,只有调用线程在子进程中存在,所有其他的Pthreads线程状态仍保留为与调用fork时相同的状态。在子进程中,线程拥有与在父进程内相同的状态。它拥有相同的互斥量,同样的线程私有数据键值等。尽管当调用fork时在同步对象上等待的任何线程不再等待,所有的互斥量和条件变量仍然存在(因为其他线程不在子进程存在,所以他们怎么能等待呢?)。
注:fork调用不会影响互斥量的状态。如果它在父进程中被锁住,则它在子进程中被锁!
如果一个互斥量在fork调用时被锁,则它在子进程中仍然被锁。因为一个加锁的互斥量被锁住它的线程拥有,只有锁住互斥量的线程是调用fork的那个线程时,互斥量可以在子进程中被开锁。这是重要的如果当你调用fork时,另外的线程把一个互斥量锁住,则你将失去对该互斥量和由该互斥量控制的任何数据的存取。
因为没有调用线程私有数据销毁和清除处理函数,你可能需要担心存储泄漏问题。
1.fork处理器
int pthread_atfork(void (*prepare)(void),void (*parent)(void),void(*child)(void));
Pthreads增加了pthread_atfork ”fork处理器”机制以允许你的代码越过fork调用保护数据和不变量。这与atexit有点类似,后者在一个进程终止时允许程序执行清除操作。使用pthread_atfork,你需要提供三个独立的处理函数地址。prepare fork处理器在父进程调用fork之前调用,parent fork处理器在fork执行后在父进程内被调用,child fork处理器在fork执行后在子进程内被调用。
通常,pthread fork处理器以正确的顺序锁住所有的由相关代码,使用的互斥量以阻止死锁的发生。调用fork的线程将在prepare fork处理器中阻塞直到它锁住了所有的互斥量后,这就保证了其他线程不能锁住某个互斥量或修改子进程可能需要的数据。parent fork处理器只需要开锁所有互斥量即可,以允许父进程和所有线程继续正常工作。
child fork处理器经常可以与parent fork处理器一样;但是有时需要重置程序或库的状态。例如:如果使用daemon线程在后台执行函数,你或者需要记录那些线程不再存在的事实,或者在子进程内创建新线程来执行同样的函数。你可能需要重置计数器,释放堆存储等。
/* * atfork.c * * Demonstrate the use of "fork handlers" to protect data * invariants across a fork. */ #include <sys/types.h> #include <pthread.h> #include <sys/wait.h> #include "errors.h" pid_t self_pid; /* pid of current process */ pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; /* * This routine will be called prior to executing the fork, * within the parent process. */ void fork_prepare (void) { int status; /* * Lock the mutex in the parent before creating the child, * to ensure that no other thread can lock it (or change any * associated shared state) until after the fork completes. */ status = pthread_mutex_lock (&mutex); if (status != 0) err_abort (status, "Lock in prepare handler"); printf("fork_prepare\n"); } /* * This routine will be called after executing the fork, within * the parent process */ void fork_parent (void) { int status; /* * Unlock the mutex in the parent after the child has been created. */ status = pthread_mutex_unlock (&mutex); if (status != 0) err_abort (status, "Unlock in parent handler"); printf("fork_parent\n"); } /* * This routine will be called after executing the fork, within * the child process */ void fork_child (void) { int status; /* * Update the file scope "self_pid" within the child process, and unlock * the mutex. */ self_pid = getpid (); status = pthread_mutex_unlock (&mutex); if (status != 0) err_abort (status, "Unlock in child handler"); printf("fork_child: self_pid = %d\n",self_pid); } /* * Thread start routine, which will fork a new child process. */ void *thread_routine (void *arg) { pid_t child_pid; int status; child_pid = fork (); if (child_pid == (pid_t)-1) errno_abort ("Fork"); /* * Lock the mutex -- without the atfork handlers, the mutex will remain * locked in the child process and this lock attempt will hang (or fail * with EDEADLK) in the child. */ status = pthread_mutex_lock (&mutex); if (status != 0) err_abort (status, "Lock in child"); printf("After fork\n"); status = pthread_mutex_unlock (&mutex); if (status != 0) err_abort (status, "Unlock in child"); printf ("After fork: %d (%d)\n", child_pid, self_pid); if (child_pid != 0) { if ((pid_t)-1 == waitpid (child_pid, (int*)0, 0)) errno_abort ("Wait for child"); } return NULL; } int main (int argc, char *argv[]) { pthread_t fork_thread; int atfork_flag = 1; int status; if (argc > 1) atfork_flag = atoi (argv[1]); if (atfork_flag) { status = pthread_atfork (fork_prepare, fork_parent, fork_child); if (status != 0) err_abort (status, "Register fork handlers"); } self_pid = getpid (); printf("main self_pid = %d\n",self_pid); status = pthread_mutex_lock (&mutex); if (status != 0) err_abort (status, "Lock mutex"); /* * Create a thread while the mutex is locked. It will fork a process, * which (without atfork handlers) will run with the mutex locked. */ status = pthread_create (&fork_thread, NULL, thread_routine, NULL); if (status != 0) err_abort (status, "Create thread"); printf("before sleep\n"); sleep (5); printf("after sleep\n"); status = pthread_mutex_unlock (&mutex); if (status != 0) err_abort (status, "Unlock mutex"); printf("main unlock\n"); status = pthread_join (fork_thread, NULL); if (status != 0) err_abort (status, "Join thread"); printf("huangcheng \n"); return 0; }
19~32 函数fork_prepare是prepare处理器。在创建子进程前,它将在父进程内被fork调用。该函数改变的任何状态(特别是被锁住的互斥量)将被拷贝进子进程。fork_prepare函数锁住程序的互斥量。
38~49 函数fork_parent是parent处理器。在创建子进程后,它将在父进程内被fork调用。总的来说,一个parent处理器应该取消在parent处理器中做的处理,以便父进程能正常继续。fork_parent函数解锁fork_prepare锁住的互斥量。
55~68 函数fork_child是child处理器。它将在子进程被fork调用。在大多数情况下,child处理器需要执行在fork_parent处理器做过的处理,解锁状态以便子进程能继续运行。它可能也需要执行附加的清除操作,例如,fork_child锁住self_pid变量为子进程的pid,同时解锁进程互斥量。
73~100 在创建子进程以后,它将继续执行thread_routine代码,thread_routine函数解锁互斥量。当运行fork处理器时,fork调用将被阻塞(当prepare处理器锁住互斥量时)直到互斥量可用。没有fork处理器,线程将在主函数解锁互斥量前调用fork,并且线程将在这个点上在子进程挂起。
117~130 主程序在创建将调用fork的线程之前锁住互斥量。然后,它睡眠若干秒以保证当互斥量被锁住时,线程能够调用fork,然后解锁互斥量。运用pthread_routine的线程将总是在父进程中成功,因为它将简单的阻塞直到主程序释放锁。
运行结果:
main self_pid = 5564
before sleep
after sleep
main unlock
fork_prepare
fork_parent
After fork
After fork: 5568 (5564)
fork_child: self_pid = 5568
After fork
After fork: 0 (5568)
huangcheng
可以调用pthread_atfork多次注册多组回调函数,这时,回调函数调用的顺序规定如下:
①prepare函数调用顺序与它们的注册顺序相反;
②parent和child函数的调用顺序与注册顺序相同。
2.exec
exec函数没有因为引入线程受到很多影响。exec函数的功能是消除当前程序的环境并且用一个新程序代替它。对exec的调用,将很快的终止进程内除调用exec的线程外的所有线程。他们不执行清除处理器或线程私有数据destructors——线程只是简单的停止存在。
所有的同步对象也消失了,除了 pshared互斥量(使用PTHREAD_PROCESS_SHARED属性值创建的互斥量)和pshared条件变量。因为只要共享内存被一些进程映射,后者仍然是有用的。然而,你应该解锁当前进程可能锁住的任何pshared互斥量——系统不会为你解锁它们。
3.进程结束
在一个非线程程序中,对于exit函数的显示调用和从程序主函数返回有一样的效果,指进程退出。Pthreads增加了pthread_exit函数,该函数能在进程继续运行的同时导致单个线程的退出。
在一个多线程程序中,主函数是“进程主线程的启动函数”。尽管从任何其他线程的启动函数返回就像调用pthread_exit终止线程一样,但是从主函数返回将终止整个进程。与进程相关的所有内存(和线程)将消失。线程不会执行清除处理器或线程私有数据destructors函数。调用exit具有同样的效果。
当你不想使用起始线程或让它等待其他线程结束时,可以通过调用pthread_exit而非返回或者调用exit退出主函数。从主函数中调用pthread_exit将在不影响进程内其他线程的前提下终止起始线程,允许他们继续和正常完成。