多线程下的信号和fork,以及IO操作

来源:互联网 发布:scientific linux 6 编辑:程序博客网 时间:2024/06/05 02:59
线程和信号
    信号十分复杂, 在本身进程基础上, 就存在信号处理. 信号屏蔽字. 信号未决等因素需要去考虑, 在遇到线程后, 信号与线程之间就更是复杂

基础认知

    每个线程都有自己的信号屏蔽字, 但是信号的处理是进程中所有线程共享的, 这就意味着单个线程可以阻止某些信号, 但当某个线程修改了与某个信号相关的处理行为后, 所有的线程都必须共享这个行为的改变.
    进程中的信号是递送到单个线程的。如果一个信号与硬件故障相关,那么该信号一般会被发送到引起该事件的线程去, 而其他的信号则会被发送到任意一个线程去.

线程信号操作
    是否还记得之前的sigpromask函数, 此函数只能用于单线程函数, 因为它会设置errno这个全局变量. 其实许多函数都是不允许在多线程情况下执行的, 他们都被称为不可重入函数. 大多数情况下,linux都提供了他们对应的可重入版本
    在线程中想要设置信号屏蔽字需要通过 pthread_sigmask
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
    用法与sigprocmask大致类似, 不需多言

    线程能通过sigwait来等待一个或多个信号的出现
int sigwait(const sigset_t *set, int *sig);
    set指定了线程等待的信号集, sig指向的整数表示接收到的信号值
    如果信号集中的某个信号在sigwait调用的时候处于挂起状态, 那么sigwait将会无阻塞的返回. 在返回前, sigwait将从进程中移除那些处于挂起等待状态的信号.
    为了避免错误行为的发生,线程在调用sigwait之前, 必须阻塞那些它正在等待的信号(即在调用sigwait之前将需要接收的信号阻塞),sigwait会在有要等待的信号被递送来之前,原子的取消信号的阻塞状态.接收到信号后,即在返回之前, sigwait还会恢复线程的信号屏蔽字
    为了防止信号中断某个线程的正常执行, 我们还可以把要接收的信号添加到每个线程的信号屏蔽字中,然后安排专用线程处理信号.

    进程之间的信号传递通过kill, 线程之间的信号传递也类似:
int pthread_kill(pthread_t thread, int sig);
    可以通过传递0值的sig来看看这个线程是否依旧存在

#include <pthread.h>#include <stdlib.h>#include <stdio.h>#include <unistd.h>#include <signal.h>int quitflag = 0;sigset_t mask;pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;pthread_cond_t cond = PTHREAD_COND_INITIALIZER;void *thr_fn(void *arg){    int err, signo;    for(;;)    {        err = sigwait(&mask, &signo);        //sigwait的一些列原子操作可以让它接收到想要的信号        if(err != 0)            exit(1);                switch(signo)                        //对接收到的信号进行处理        {        case SIGINT:            printf("\ninterrupt\n");            break;        case SIGQUIT:                //接收到此信号就修改quitflag唤醒条件变量终止整个进程            printf("get\n");            pthread_mutex_lock(&lock);            quitflag = 1;            pthread_mutex_unlock(&lock);            pthread_cond_signal(&cond);            break;        default:            break;        }    }}int main(){    int err;    sigset_t oldmask;    pthread_t tid;    sigemptyset(&mask);        sigaddset(&mask, SIGINT);    sigaddset(&mask, SIGQUIT);        //这么设置后由此线程产生的所有子线程都会继承相同的信号屏蔽字    if((err=pthread_sigmask(SIG_BLOCK, &mask, &oldmask)) != 0)          exit(-1);        err = pthread_create(&tid, NULL, thr_fn, 0);    if(err != 0)        exit(-2);        pthread_mutex_lock(&lock);    while(quitflag == 0)    {        pthread_cond_wait(&cond, &lock);    }    pthread_mutex_unlock(&lock);    //...        if(sigprocmask(SIG_UNBLOCK, &oldmask, NULL) != 0)        exit(-3);        return 0;    return 0;}
   

子线程继承了主线程的信号屏蔽字,但是在调用sigwait后就能接收信号了, 因此此时就只有一个线程能接收信号, 从而不必担心其他线程因为信号的到来而导致中断




线程和fork
    当线程调用fork后, 就为子进程创建了整个进程地址空间的副本
    子进程通过继承整个整个地址空间的副本, 还从父进程那儿继承了每个互斥量, 读写锁, 和条件变量的状态. 如果父进程包含一个以上的线程, 子进程在fork返回以后, 如果紧接着不是马上调用exec的话, 就需要清理锁状态
    在子进程内部, 只存在一个线程, 它是由父进程中调用fork的线程的副本构成的. 如果父进程中的线程占有锁, 子进程将同样占有这些锁. 问题是子进程并不包含占有锁的线程的副本, 所以子进程没有办法知道它占有了哪些锁, 需要释放哪些锁
    如果在子进程中立刻运行exec系列的函数, 那么这些问题可以被避免. 旧的地址空间将被抛弃, 所以锁的状态无关紧要. 但如果子进程需要继续做处理工作, 就要使用其他策略
    在多线程的进程中, 为了避免不一致的状态, 在fork返回和子进程调用其中一个exec函数之间, 子进程只能调用异步信号安全的函数, 即在调用exec之前进程处理不能涉及锁状态

    要清除锁状态, 可以通过 pthread_atfork函数建立fork处理程序
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
    此函数可以装3个处理锁的函数
    prepare函数由父进程在fork创建子进程前调用. 这个fork处理程序的任务是获取父进程定义的所有锁
    parent函数是在fork创建子进程后,返回之前在父进程上下文中调用的. 是对prepare中获取的所有锁进行解锁
    child函数在fork返回之前在子进程中释放所有锁
    看如下例子:
#include <stdio.h>#include <pthread.h>#include <stdlib.h>#include <unistd.h>pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;void prepare(void){    //获取所有锁    int err;    printf("preparing ... \n");    if((err=pthread_mutex_lock(&lock1)) != 0)        exit(-1);    if((err=pthread_mutex_lock(&lock2)) != 0)        exit(-1);}void parent(void){    //释放所有锁    int err;    printf("parenting ... \n");    if((err=pthread_mutex_unlock(&lock1)) != 0)        exit(-1);    if((err=pthread_mutex_unlock(&lock2)) != 0)        exit(-1);}void child(void){    //释放所有锁    int err;    printf("childing ... \n");    if((err=pthread_mutex_unlock(&lock1)) != 0)        exit(-1);    if((err=pthread_mutex_unlock(&lock2)) != 0)        exit(-1);}void *thr_fn(void *arg){    printf("child threading...\n");    pause();    printf("child thread over...\n");    exit(0);}int main(){    int err;    pid_t pid;    pthread_t tid;    //设置好fork前后需要调用的函数    if((err=pthread_atfork(prepare, parent, child)) != 0)        exit(-1);        if((err=pthread_create(&tid, NULL, thr_fn, NULL)) != 0)        exit(-1);    sleep(2);    printf("parent about to fork()...\n");    if((pid=fork()) < 0)        exit(-1);    else if(pid == 0)        printf("child process after fork)(\n");    else        printf("parent process after fork()\n");        return 0;}


$ ./pachild threading...parent about to fork()...preparing ...parenting ...parent process after fork()childing ...child process after fork)(

由此运行结果可以看出提前设置好的函数的运行顺序.


要知道的是, 可以多次调用pthread_atfork函数从而设置多套fork处理程序, 如果不需要其中某个处理程序, 可以给特地该的处理程序参数传入空指针,他就不起作用了
使用多个fork处理程序时, 处理程序的调用顺序并不相同. parent和child处理程序是以他们注册时的顺序进行调用的, 而prepare函数则是与他们注册的顺序正好相反. 这样可以允许多个模块注册他们自己的fork处理程序, 而且可以保持锁的层次

这里有一个例子帮助理解:
    加入模块A需要调用模块B(我们知道, 在C语言中, 某函数需要调用其他函数的话, 其他函数必须在该函数之前被声明), 而且每个模块都有自己的一套锁. 若A调用B. 模块B必须在模块A之前设置它的fork处理程序. 当父进程调用fork时, 就会执行下步骤(假设子进程在父进程之前先执行):
1. 调用A模块的prepare函数获取A的所有锁
2. 调用B模块的prepare函数获取A的所有锁
3. 创建子进程
4. 调用B模块的child 函数给子进程释放所有获取的锁
5. 调用A模块的child 函数给子进程释放所有获取的锁
6. fork函数返回到子进程
7. 调用B中的parent 函数给父进程释放所有获取的锁
8. 调用A中的parent 函数给父进程释放所有获取的锁
9. fork函数返回父进程

虽然, pthread_atfork意图是使fork之后的锁状态保持一致, 但它还是存在一些不足之处
    只有对锁的处理, 没有对较复杂的同步对象(条件变量,屏障)等的状态处理
    递归互斥量不能在child程序中清理, 因为没有办法确定加锁的次数
    等等...

多线程IO

如果几个线程要对同一个文件进行处理的话, 那么pread和pwrite函数是十分有用的

ssize_t pread(int fd, void *buf, size_t count, off_t offset);ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
他们在单线程的情况下相当与先调用lseek, 之后再调用read, write; 然而在多线程情况下, 却因为他们的原子性体现出了优势
0 0