APUE——线程控制之线程和fork

线程控制之线程和fork
fork()函数与Linux中的多线程编程
使用 Mutex 实现进程间同步
fork

  1. 子进程通过继承整个地址空间的副本,也从父进程那里继承了所有互斥量、读写锁和条件变量的状态。如果父进程包含多个线程,子进程在fork返回以后,如果紧接着不是马上调用exec的话,就需要清理锁状态。
  2. 在子进程内部只存在一个线程,它是由父进程中调用fork的线程的副本构成的。如果父进程中的线程占有锁,子进程同样占有这些锁。问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法知道它占有了哪些锁并且需要释放哪些锁。
    在这里插入图片描述

1、 fork()与多线程

  1. 虽然只将发起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.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是不能跨进程使用的!

  1. 如果父进程有多个线程,其中主线程进行fork,但是其他线程中,调用了互斥锁,那么子进程只包含了父进程的主线程,和其他子线程的互斥量,所以其并不知道子进程中的互斥量是否上锁,如果再次上锁,可能死锁。
  2. 如果父进程中的线程占有锁,子进程同样占有这些锁。问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法知道它占有了哪些锁并且需要释放哪些锁。
  3. 如果子进程从fork返回以后马上调用某个exec函数,就可以避免这样的问题。
  4. 注意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));
  1. prepare fork处理程序由父进程在fork创建子进程前调用,这个fork处理程序的任务是获取父进程定义的所有锁。
  2. parent fork处理程序是在fork创建了子进程以后,但在fork返回之前在父进程环境中调用的,这个fork处理程序的任务是对prepare fork处理程序获得的所有锁进行解锁。
  3. child fork处理程序在fork返回之前在子进程环境中调用,与parent fork处理程序一样,child fork处理程序也必须释放prepare fork处理程序获得的所有锁。

例子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 …,因为子进程中没有退出!

猜你喜欢

转载自blog.csdn.net/weixin_44537992/article/details/106146394