线程控制之线程和fork
fork()函数与Linux中的多线程编程
使用 Mutex 实现进程间同步
fork
- 子进程通过继承整个地址空间的副本,也从父进程那里继承了所有互斥量、读写锁和条件变量的状态。如果父进程包含多个线程,子进程在fork返回以后,如果紧接着不是马上调用exec的话,就需要清理锁状态。
- 在子进程内部只存在一个线程,它是由父进程中调用fork的线程的副本构成的。如果父进程中的线程占有锁,子进程同样占有这些锁。问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法知道它占有了哪些锁并且需要释放哪些锁。
1、 fork()与多线程
- 虽然只将发起fork()调用的线程复制到子进程中,但全局变量的状态以及所有的pthreads对象(如互斥量、条件变量等)都会在子进程中得以保留,这就造成一个危险的局面。例如:一个线程在fork()被调用前锁定了某个互斥量,且对某个全局变量的更新也做到了一半,此时fork()被调用,所有数据及状态被拷贝到子进程中,那么子进程中对该互斥量就无法解锁(因为其并非该互斥量的属主),如果再试图锁定该互斥量就会导致死锁,这是多线程编程中最不愿意看到的情况。同时,全局变量的状态也可能处于不一致的状态,因为对其更新的操作只做到了一半对应的线程就消失了。fork()函数被调用之后,子进程就相当于处于signal handler之中,此时就不能调用线程安全的函数(用锁机制实现安全的函数),除非函数是可重入的,而只能调用异步信号安全(async-signal-safe)的函数。fork()之后,子进程不能调用:
1、malloc(3)。因为malloc()在访问全局状态时会加锁。
2、任何可能分配或释放内存的函数,包括new、map::insert()、snprintf() ……
3、任何pthreads函数。你不能用pthread_cond_signal()去通知父进程,只能通过读写pipe(2)来同步。
4、printf()系列函数,因为其他线程可能恰好持有stdout/stderr的锁。
5、除了man 7 signal中明确列出的“signal安全”函数之外的任何函数。
- 因为并未执行清理函数和针对线程局部存储数据的析构函数,所以多线程情况下可能会导致子进程的内存泄露。另外,子进程中的线程可能无法访问(父进程中)由其他线程所创建的线程局部存储变量,因为(子进程)没有任何相应的引用指针。
1.1 Mutex与多进程通信
我们知道 Mutex 互斥量是可以用在线程间同步的,线程之间共享进程的数据,mutex 就可以直接引用。而进程有自己独立的内存空间,要怎样将它应用在进程间同步呢?为了达到这一目的,可以在 pthread_mutex_init 初始化之前,修改其属性为进程间共享,并将其映射到共享内存中即可。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <string.h>
/* 定义 mutex */
pthread_mutex_t mutex;
pthread_mutexattr_t mutexattr;
int main()
{
int shm_id = 0;
int i = 0;
pid_t pid;
pthread_mutex_t *m_mutex;
/* mutex attr 初始化 */
pthread_mutexattr_init(&mutexattr);
/* 修改属性 */
pthread_mutexattr_setpshared(&mutexattr, PTHREAD_PROCESS_SHARED);
/* mutex 初始化 */
pthread_mutex_init(&mutex, &mutexattr);
/* 申请共享内存 */
shm_id = shmget((key_t)1004, sizeof(mutex), IPC_CREAT | 0600);
/* 映射共享内存到进程地址空间 */
m_mutex = (pthread_mutex_t*)shmat(shm_id, 0, 0);
memcpy(m_mutex, &mutex, sizeof(mutex));
pid = fork();
if(pid == 0){
pthread_mutex_lock(m_mutex);
for(; i<3; i++){
pthread_mutex_lock(m_mutex);
puts("This is the child process!");
}
}else if(pid > 0){
for(; i<3; i++){
sleep(1);
pthread_mutex_unlock(m_mutex);
puts("This is the parent process!");
}
/* 回收子进程资源 */
wait(NULL);
}
/* 销毁 mutex */
pthread_mutexattr_destroy(&mutexattr);
pthread_mutex_destroy(&mutex);
return 0;
}
1.2 分析代码
互斥量mutex的shared和共享内存必须同时使用,才能保证mutex能够跨进程使用!!!
#include "apue.h"
#include "myerror.h"
// #include <pthread.h>
// #include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <string.h>
pthread_mutexattr_t mutexattr;
pthread_mutex_t mutex ;
pthread_attr_t pattr1;
pthread_cond_t pcond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t *m_mutex;
int i=0;
void init()
{
if(pthread_mutexattr_init(&mutexattr)!=0)
err_sys("pthread_mutexattr_init error\n");
pthread_mutexattr_setpshared(&mutexattr,PTHREAD_PROCESS_SHARED);
if(pthread_mutex_init(&mutex,&mutexattr)!=0)
err_sys("pthread_mutex__init error\n");
pthread_attr_init(&pattr1);
}
void* fun1(void* attr)
{
puts("hello world?????");
pthread_mutex_lock(&mutex);
pthread_cond_wait(&pcond,&mutex);
i++;
pthread_mutex_unlock(&mutex);
puts("hello world");
return NULL;
}
int main()
{
int err = 0;
pid_t pid = 0;
pthread_t tid1 = 0;
int shm_id = 0;
printf("start init");
init();
printf("start init");
shm_id = shmget((key_t)1008, sizeof(mutex), IPC_CREAT | 0600);
/* 映射共享内存到进程地址空间 */
m_mutex = (pthread_mutex_t*)shmat(shm_id, 0, 0);
if (memcpy(m_mutex, &mutex, sizeof(mutex))<0)
{
perror("error");
}
err = pthread_attr_setdetachstate(&pattr1,PTHREAD_CREATE_DETACHED);
if(err == 0)
pthread_create(&tid1,&pattr1,fun1,NULL);
//pthread_mutex_lock();
pid=fork();
if (pid<0)
{perror("fork error");}
if(pid == 0)
{
//pthread_mutex_unlock(&mutex);
sleep(2);
pthread_mutex_lock(m_mutex);
puts("this is child thread");
pthread_cond_signal(&pcond); //可以获得互斥量,但是发送信号是有没有用的,子线程没有拷贝过去
//,全部终止了,且没有运行清理函数,只能通过读写pipe(2)来同步
pthread_mutex_unlock(m_mutex);
//exit(0);
}
else if(pid>0)
{
sleep(1); //这里必须让控制线程sleep 1s,保证在发送信号之前,pthread_cond_wait运行结束!
//pthread_mutex_lock(&mutex);
puts("this is parent thread");
pthread_cond_signal(&pcond);
puts("this is parent thread22");
}
wait(NULL);
pthread_mutexattr_destroy(&mutexattr);
pthread_mutex_destroy(&mutex);
pthread_attr_destroy(&pattr1);
return(0);
}
2 pthread_atfork函数
个人理解,pthread_atfork函数是为了在子进程生成前,先显式获取子进程可能冲突的互斥量,然后再解锁,其与直接在子进程中解锁互斥量的作用相同!而且互斥量如果没有共享内存和shared是不能跨进程使用的!
- 如果父进程有多个线程,其中主线程进行fork,但是其他线程中,调用了互斥锁,那么子进程只包含了父进程的主线程,和其他子线程的互斥量,所以其并不知道子进程中的互斥量是否上锁,如果再次上锁,可能死锁。
- 如果父进程中的线程占有锁,子进程同样占有这些锁。问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法知道它占有了哪些锁并且需要释放哪些锁。
- 如果子进程从fork返回以后马上调用某个exec函数,就可以避免这样的问题。
- 注意fork之后,子进程的代码是从fork函数开始,直到exit
#include <pthread.h>
// Upon successful completion, pthread_atfork() shall return a value of zero; otherwise, an error number shall be returned to indicate the error.
// @prepare 新进程产生之前被调用
// @parent 新进程产生之后在父进程被调用
// @child 新进程产生之后,在子进程被调用
int pthread_atfork (void (*prepare) (void), void (*parent) (void), void (*child) (void));
- prepare fork处理程序由父进程在fork创建子进程前调用,这个fork处理程序的任务是获取父进程定义的所有锁。其核心用法是,在父进程中,明确获得锁,如果父进程中子线程中获得了锁,那么主线程就等待,直到子线程解除锁之后,prepare fork获得锁,再调用fork,避免父进程中其他线程获得锁,影响子进程
- parent fork处理程序是在fork创建了子进程以后,但在fork返回之前在父进程环境中调用的,这个fork处理程序的任务是对prepare fork处理程序获得的所有锁进行解锁,核心用法是,解除父进程中prepare获得的锁。
- child fork处理程序在fork返回之前在子进程环境中调用,与parent fork处理程序一样,child fork处理程序也必须释放prepare fork处理程序获得的所有锁,核心用法是,解除父进程中prepare获得的锁。
例子1
#include "apue.h"
#include <pthread.h>
pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;
void
prepare(void)
{
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");
pause();
return(0);
}
int
main(void)
{
int err;
pid_t pid;
pthread_t tid;
#if defined(BSD) || defined(MACOS)
printf("pthread_atfork is unsupported\n");
#else
if((err = pthread_atfork(prepare, parent, child)) != 0)
err_exit(err, "can't install fork handlers");
err = pthread_create(&tid, NULL, thr_fn, 0);
if(err != 0)
err_exit(err, "can't create thread");
sleep(2);
printf("parent about to fork...\n");
if((pid = fork()) < 0)
err_quit("fork failed");
else if(pid == 0) /* child */
printf("child returned from fork\n");
else /* parent */
printf("parent returned from fork\n");
#endif
exit(0);
}
例子2
prepare里先解锁,然后子线程上锁,
#include <stdio.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *doit(void *arg)
{
printf("pid = %d begin doit ...\n", static_cast<int>(getpid()));
pthread_mutex_lock(&mutex);
struct timespec ts = {2, 0};
nanosleep(&ts, NULL);
pthread_mutex_unlock(&mutex);
printf("pid = %d end doit ...\n", static_cast<int>(getpid()));
return NULL;
}
void prepare(void)
{
pthread_mutex_unlock(&mutex);
}
void parent(void)
{
pthread_mutex_lock(&mutex);
}
int main(void)
{
pthread_atfork(prepare, parent, NULL);
printf("pid = %d Entering main ...\n", static_cast<int>(getpid()));
pthread_t tid;
pthread_create(&tid, NULL, doit, NULL);
struct timespec ts = {1, 0};
nanosleep(&ts, NULL);
if (fork() == 0)
{
doit(NULL);
}
pthread_join(tid, NULL);
printf("pid = %d Exiting main ...\n", static_cast<int>(getpid()));
return 0;
}
pid = 4905 Entering main ...
pid = 4905 begin doit ...
pid = 4908 begin doit ...
pid = 4905 end doit ...
pid = 4905 Exiting main ...
pid = 4908 end doit ...
pid = 4908 Exiting main ...
上例可以看出,子进程也退出,且运行了Exiting main …,因为子进程中没有退出!