线程和信号、线程的进程之间的控制的关系以及线程实现模型说明
- 线程栈:创建线程的时候,每个线程都有一个属于自己的线程栈,且大小固定,在linux/X86架构上面,除去主线程外,堆栈的默认大小为2MB,当然也可以创建线程的时候,通过相关函数来改变
1.1 线程和信号的关系
-
线程和信号:
- 进程中某一个线程收到了未经处理的信号,且其默认动作是stop或者terminate,那么将停止或者终止所有该进程的所有线程(进程层面)
- 如果某一个线程使用sigaction为某类信号创建了处理函数,那么当收到该信号的时候,任何线程都会去调用处理函数,如果设置了忽略,所有线程都会忽略该信号
- 信号可以发送给进程,也可以发送给特定线程,满足下列情况:
- 信号产生源于线程上下文中特定硬件指令执行
- 当线程试图对已断开的管道进行读写产生的SIGPIPE信号
- pthread_kill和pthread_sigqueue允许向同一进程下的线程发送信号
- 其它机制产生的信号都是面向进程的
- 如果多线程程序收到一个程序,且该进程已经为此信号建立了信号处理程序,内核会任意选择一条线程来接收这一个信号,并且由该进程调用信号处理程序
- 信号掩码是针对每个线程而言的,使用pthread_sigmask(),各线程可以独立阻止或者放行各种信号
- 针对进程和每条线程的挂起信号,内核都会为其分别维护一个记录,sigpending()返回整个进程和当前线程挂起的信号的并集
- 信号终端了pthread_mutex_lock()调用,会自动重启,如果信号中断了对pthread_cond_wait()的调用,要么重新开始,要么返回0,,表示遭遇假唤醒,所以才需要设置重新检查相应的判断条件并且重新发起调用
- 备选信号栈新线程,不会从创建者线程那里继承来
-
操作线程信号掩码:
#include <signal.h> /***************************** 函数功能:修改或者获取当前的线程的信号掩码,与sigprocmask()函数使用方法相同,只是前者针对线程,在多线程中必须使用pthread_sigmask 注意:新创建的线程会从创建者线程处拷贝一份信号掩码的备份 *****************************/ int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
-
向线程发送信号:
#include <signal.h> /***************************************** 函数功能:向同一个进程下的线程发送信号 返回值:On success, pthread_kill() returns 0; on error, it returns an error number, and no signal is sent. *****************************************/ int pthread_kill(pthread_t thread, int sig); //同一进程中可以保证线程ID的唯一性 #include <pthread.h> /***************************************** 函数功能:向同一个进程下的线程发送信号,并且携带数据,相当于pthread_kill和pthread_sigqueue结合 返回值:On success, pthread_sigqueue() returns 0; on error, it returns an error number. *****************************************/ int pthread_sigqueue(pthread_t *thread, int sig, const union sigval value);
-
妥善处理异步信号:多线程编程中尽量避免信号的使用,否则应该采用如下方式去处理
-
没有任何Pthread API属于异步信号安全函数,所以在多线程编程中,应该妥善处理异步信号:
-
所有线程都阻塞进程可能收到的所有异步信号,一般在主线程阻塞,后面新创建的线程会从主线程那里继承信号掩码
-
创建一个专用线程,利用sigwait和sigwaitinfo等函数专门来接受指定信号,收到信号后安全的修改共享资源,就条件变量发出信号等
/*************************** 函数功能:等待指定信号集中任一信号的到达,并且接受该信号 ***************************/ #include <signal.h> int sigwait(const sigset_t *restrict set, int *restrict sig)
-
-
1.2 线程的进程之间的控制关系
- 线程和exec():任何一个线程调用了exec系列函数,除去调用线程,其它任何线程都会消失,没有任何线程会针对特有数据执行解构函数,也不会调用清理函数,然后调用线程的线程ID也是不确定的
- 线程和fork():
- 仅仅对发起调用的线程复制到子进程中子进程中线程ID与父进程中发起fork调用的线程ID相一致,其它线程在子进程都会消失,也不会为这些消失的线程调用清理函数以及针对特有数据的解析函数,导致的一些问题:
- 全局变量的状态和以及所有Pthreads对象都会在子进程保存,假如原进程中某一个线程刚刚锁定了一个互斥量,全局变量也更新到一半,子进程中并不能解锁互斥量,因为不是同一个属主,全局变量的访问也不对,因为原来那个线程可能消失了
- 可能导致子进程的内存泄漏
- 解决办法1:fork调用之后,马上执行exec()的调用
- 解决方法2:fork处理函数pthread_atfork(prepare_func, parent_func, child_func)
- 每次pthread_atfork会将prepare_func添加到一个函数列表,调用fork之前会按照注册的相反顺序执行prepare_func函数,parent_func和child_func也添加到一个函数列表中去,fork返回之前分别在父、子进程中(按照注册顺序自动运行)
- 仅仅对发起调用的线程复制到子进程中子进程中线程ID与父进程中发起fork调用的线程ID相一致,其它线程在子进程都会消失,也不会为这些消失的线程调用清理函数以及针对特有数据的解析函数,导致的一些问题:
- 线程和exit():任何线程调用exit或者主线程执行了return,那么所有线程都会消失,也不会执行线程特有数据的解构或者清理函数
1.4 线程实现模型
-
(KSE:Kernel Scheduling Entity):内核调度实体,内核分配CPU以及其它系统资源的对象单位
- 多对一实现(用户级线程):线程的创建、调度以及同步都在进程内用户空间的线程库来完成,内核根本不知道线程的存在,此时进程是KSE
- 优点:
- 线程操作速度快、无需频繁切换到内核模式
- 系统移植比较方便
- 缺点:
- 一个线程阻塞、会导致整个进程被阻塞
- 内核不知道线程的存在、也就无法在多处理器上并发运行
- 优点:
- 一对一实现(内核级线程)
- 线程现在是KSE,解决了用户级线程的所有缺陷,但是线程的切换消耗非常大,有大量线程存在的情况下,对COU的负担很大,可能降低系统性能,尽管有这些缺点、但是性能胜于用户级线程LLinuxThreads和NPTL都采用这种模型
- 多对多实现:结合了多对一和一对一的优点,但是实现困难,故否定了这一模型
- 多对一实现(用户级线程):线程的创建、调度以及同步都在进程内用户空间的线程库来完成,内核根本不知道线程的存在,此时进程是KSE
-
Linux下POSIX线程的实现:
- LinuxThreads:这是针对linux线程最初实现,基本淘汰,由NPTL代替
- NPTL(Native POSIX Thread Library):linux现在的线程调度实现方法,采用一对一模型实现
-
命令getconf GNU_LIBPTHREAD_VERSION:当前系统使用的是什么线程实现方式
-
命令$(ldd /bin/ls | grep libc.so | awk ‘{print $3}’) | egrep -i ‘threads|nptl’:线程实现信息