- 库函数中的初始化使用
pthread_once()
,PTHREAD_ONCE_INIT
linux
中once
分为三个状态NEVER(0)
、IN_PROGRESS(1)
、DONE(2)
- 若是once_control初值为0,那么pthread_once从未执行过,init_routine()函数会执行。
- 若是once_control初值设为1,则因为全部pthread_once()都必须等待其中一个激发"已执行一次"信号, 所以全部pthread_once ()都会陷入永久的等待中,init_routine()就没法执行
- 若是once_control设为2,则表示pthread_once()函数已执行过一次,从而全部pthread_once()都会当即 返回,init_routine()就没有机会执行 当pthread_once函数成功返回,once_control就会被设置为2。
#include <pthread.h>
pthread_once_t once = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t* once_control, void (*init_routine)(void));
复制代码
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
pthread_once_t once = PTHREAD_ONCE_INIT;
pthread_t tid;
void thread_init() {
sleep(2);
printf("*********I'm in thread 0x%x\n", tid);
}
void *thread_fun2(void *arg) {
tid = pthread_self();
printf("I'm thread 0x%x\n", tid);
printf("once is %d\n", once);
pthread_once(&once, thread_init);
printf("once is %d\n", once);
return NULL;
}
void *thread_fun1(void *arg) {
sleep(1);
tid = pthread_self();
printf("I'm thread 0x%x\n", tid);
printf("*****once is %d\n", once);
pthread_once(&once, thread_init);
return NULL;
}
int main() {
pthread_t tid1, tid2;
int err;
err = pthread_create(&tid1, NULL, thread_fun1, NULL);
if(err != 0)
{
printf("create new thread 1 failed\n");
return ;
}
err = pthread_create(&tid2, NULL, thread_fun2, NULL);
if(err != 0)
{
printf("create new thread 1 failed\n");
return ;
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
/ *
I'm thread 0x81bf1700
once is 0
I'm thread 0x823f2700
*****once is 1
*********I'm in thread 0x823f2700
once is 2
*/
复制代码
- 避免了修改不可重入函数的参数
#include<pthread.h>
int pthread_key_create(pthread_key_t *key, void(*destructor)(void *));
// 解构函数destructor, 线程终止时将key的关联值做为参数转给destructor
int pthread_setspecific(pthread_key_t key, const void * value);
// 通常value 是指向调用者分配的一块内存,线程终止时会将value 传给key对应的解构函数
int pthread_getspecific(pthread_key_t key);
// 返回当前线程绑定的value
复制代码
key在当前进程的存贮linux
不一样线程中的数据缓冲区,线程刚刚建立时会初始化为null算法
通常流程shell
- 函数建立一个key,经过传参调用
pthread_key_create()
的函数做为pthread_once()
的参数,调用pthread_once()
保证一次建立key的行为- 经过
pthread_setspecific()
和pthread_getspecific()
来绑定和确认key又没用绑定线程独立缓冲区,没有的话malloc()分配一次,只会有一次。
static __thread buf[size];
复制代码
__thread
需紧跟extern
或static
后面编程
#include<pthread.h>
int pthread_cancel(pthread_t thread);
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
复制代码
设置线程取消状态和类型vim
- STATE 能否取消
- PTHREAD_CANCEL_DISABLE 挂起请求直到,取消状态启用
- PTHREAD_CANCEL_ENABLE
- TYPE
- PTHREAD_CANCEL_ASYNCHRONOUS, 可能会在任什么时候点取消
- PTHREAD_CANCEL_DEFERED 取消请求挂起直到取消点
取消点缓存
- SUSv3规定了一些必须有取消点的函数
- 线程一点接受了取消信息,启用取消性状态而且类型置位延迟,其会在下次到达取消点终止,若没有detach,为了防止成为僵尸线程,必须由其余线程对其进行连接,链接以后
pthread_join()
中的第二个参数将会是:PTHREAD_CANCELD
# include<pthread.h>
void pthread_testcancel(void);
//线程的代码中没有取消点时,能够经过调用其做为取消点
复制代码
清理函数安全
# include<pthread.h>
void pthread_cleanup_push(void (*routine)(void*), void *arg);
void pthread_cleanup_pop(int execute);
复制代码
异步取消bash
- 可异步取消线程不该该分配任何资源,也不能获取互斥量和锁
- 可以使用在取消计算密集型的循环的线程
线程栈多线程
- 线程栈的大小能够经过建立线程时的
pthread_attr_t
类型参数设定,线程栈越大,受制于用户模式虚拟内存,并行的线程越少
线程和信号架构
- 信号动做属于进程层面。ex:若是进程的某一线程接收到任何未经特殊处理的信号,其缺省动做为STOP或TERMINATE,将终止该进程的全部线程
- 对信号的处置属于进程层面。ex:IGNORE
- 信号的发送既可针对整个进程,也可针对某个特定线程
- 面向线程的状况:
- 信号的产生源于线程上下文中对于特定硬件指令的执行(硬件异常:SIGBUG,SIGFPG,SIGILL,SIGSEGV)
- 线程试图对已断开的管道进行写操做产生的SIGPIPE信号
- 由
thread_kill()
或者pthread_sigqueue()
发出的信号- 多线程程序收到一个信号时,有对应处理程序时,内核会选一个线程处理该信号
- 信号掩码针对每一个线程
- 针对整个进程挂起的信号,和每条线程挂起的信号,内核有维持记录,
sigpending()
会返回整个进程和当前线程挂起信号的并集。新线程,现成的挂起信号初始值为空- 信号中断了
thread_mutex_lock()``thread_mutex_wait()
的调用,则起调用会从新开始- 备选信号栈是线程独有
#include<signal.h>
int pthread_sigmask(int how, const sigset_t *set, sigset_t *old_set);
//除了操做的是线程掩码,其余和sigprocmask()一致,多线程调用后者可能致使未定义问题
int pthread_kill(pthread_t thread, int sig);
//本进程线程发送信号
int pthread_sigqueue(pthread_t thread, int sig, const union sigval value);
//进程发送实时信号
复制代码
异步信号的处理:阻塞全部线程,专有线程去处理
- 线程和
exec()
,调用程序将被替换,除了调用线程以外,其余线程会当即消失,不会对线程特有数据额结构进行析构,也不会调用清理函数- 线程和
fork()
,只会讲调用线程fork()
到子进程中,其余线程马上消失
- 会致使其余线程的锁未释放,子进程的线程阻塞
- 不会对线程特有数据额结构进行析构,也不会调用清理函数,致使子进程内存泄漏
- 建议多线程调用
fork()
以后马上调用exev()
- 线程和
exit()
如何线程执行了exit()
或主线程执行了return,全部线程消失,不会对线程特有数据额结构进行析构,也不会调用清理函数
- 多对一:线程建立的,调度,同步的全部细节由进程内用户空间的线程库处理(相似携程?)
- 速度快,无需内核态切换
- 移植相对方便
- 当一个线程发起内核调用阻塞时,全部线程阻塞
- 内核感知不到县城,没法调度给不一样的CPU,没法调整线程优先级
- 一对一
- 避免了多对一弊端
- 可是维护每个KSE须要开销,增长内核调度器的负担
- 进程建立切换等操做比多对一慢
- 多对多
- 每一个进程拥有多个KSE, 而且能够把多个线程映射到一个KSE,权衡商量中模型
- 可是模型过于复杂,调度由线程库和内核共同实现
Linux POSIX的实现
- LinuxThread(旧)
- NPTL(一对一)
- 进程组是一组相关进程的集合
- 会话是一组相关进程组的集合
进程组
- 一个进程组拥有一个进程组的首进程,该进程是建立这个进程组的进程,其进程ID是进程组的ID
- 开始首进程建立组,结束语最后一个成员进程退出组
- 进程组首进程无需最后退出
- 新进程继承父进程的进程组ID
- 特有属性
- 特定进程组中父进程可以等待任意子进程26.1.2
- 信号能发给进程组中全部成员20.5
会话
- 会话首进程是建立新会话的进程, 其进程ID成为会话ID
- 新进程会继承父进程的会话ID
- 在任意时刻,会话中的其中一个进程组会成为终端的前台进程组,其余为后台进程组
- 当到控制终端的链接创建起来以后,会话首进程会成为该终端的控制进程
- 从shell中发出的某个命令或者经过管道链接的一组命令或致使一个或多个进程建立,并被放到一个新的进程组中
![]()
#include<unistd.h>
pid_t getgrp(void);
int setpgid(pid_t pid, pid_t pgid);
//如下等价将调用进程的进程组ID设为调用进程的PID
setpgid(0, 0);
setpgid(getpid(), 0);
setpgid(getpid(), getpid());
复制代码
pid 参数只能指定调用进程或其子进程 调用进程,pid指定进程,以及目标进程组需属于同一会话 pid不能指定会话首进程 一个进程在其子进程执行过
exec()
后没法修改子进程的进程组ID
# define _XOPEN_SOURCE 500
# include<unistd.h>
pid_t getsid(pid_t pid);
// pid为0返回调用进程的会话ID
pid_t setsid(void);
// 调用进程不能为进程组的首进程
复制代码
- 调用进程会成为新会话的首进程和该会话中新进程组的首进程
- 调用进程没有控制终端,全部以前到控制终端的链接都会断开
- 调用进程不能为进程组的首进程,若是能够的话,其进程组的原其余进程的进程组ID会被动成为另外一个会话的进程ID,破坏了会话和进程组之间严格的两级层次,进程组的全部成员必须属于同一会话
fork()建立一个新进程时,内核会确保其PID不会和已有进程的进程组ID和会话ID相同
$ ps -p $$ -o 'pid pgid sid command'
PID PGID SID COMMAND
10217 10217 10217 -bash
# $$ 是shell的PID
$ ./sid
PID=11762, PGID=11762, SID=11762
error in open # setsid以后进程再也不拥有控制终端
复制代码
#define _XOPEN_SOURCE 500
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
int main (int argc, char *argv[]){
if (fork()!=0){
_exit(0);
}
if (setsid()==-1){
printf("error in setsid");
}
printf("PID=%ld, PGID=%ld, SID=%ld\n", (long) getpid(), (long) getpgrp(), (long) getsid(0));
if (open("/dev/tty", O_RDWR)==-1){
printf("error in open");
}
return 0;
}
复制代码
- 一个会话中的全部进程可能会拥有一个控制终端
- 会话首进程首次打开一个还没成为某个会话控制终端的终端时会创建其控制终端,同时首进程成为控制进程
- 控制终端断开后,内核会想控制进程发送一个
SIGHUP
信号- 除非在调用
open(
)时指定O_NOCTTY
, 不然一个中端只能成为一个会话的控制终端- 控制终端会在
fork()
时集成,在exec()
时保持
ioctl(fd, TIOCNOTTY)
会删除进程与文件描述符df指定终端的联系,若是调用进程是终端的控制进程
- 会话中全部进程失去与控制终端的联系
- 内核会向前台进程组的全部成员发送一个SIGHUP信号(和一个SIGCONT信号)通知控制终端的丢失
- SIGHUP信号 默认行为是终止进程,若忽略此信号,进程后续从终端读取数据的请求会抛出异常
- 出如今:
- 终端驱动器检测到链接断开
- 终端窗口被关闭:(终端窗口关联的伪终端的主测文件描述符被关闭了)
shell中处理
SIGHUP
信号(关联行为)
nohup
命令能够用来使一个命令对SIGHUP的处置置位SIG_IGNdisown
(bash)会从shell的任务列表删除一个任务,这样其在shell终止时不会收到SIGHUP- shell一般会是终端的控制进程
- shell收到SIGHUP只会发SIGHUP给由它建立的进程组的进程(前台和后台进程)(若是子进程新建了个进程组不会被通知)
exec
会致使shell执行一个exec()
使指定程序替代本身 与shell不一样,若是由于终端断开引发的向控制进程发送的SIGHUB信号会致使控制进程终止,那么SIGHUB会发送给终端的前台进程组全部成员
fg %1
bg %1
复制代码
%%
,%+
指当前做业%-
指上一次做业
- 后台进程尝试从终端读会受到SIGTTIN信号,中止做业
- 终端设置了TOSTOP命令,后台进程尝试向终端输出时,会收到SIGTTOU信号,中止做业
vim 这个程序在SIGSTP和SIGCONT须要额外的操做保持终端屏幕内容
若是一个进程组变成了孤儿进程组中并包含许多被中止进程,SUSv3规定,系统会像进程组中全部成员发送SIGHUB,通知他们和会话断开,再发送SIGCONT确保他们恢复执行
linux进程调度使用CPU默认是循环时间共享,每一个进程轮流使用CPU,这段时间被称为时间片
- 公平性:每一个进程都有机会使用CPU
- 响应度:一个进程在使用CPU以前无需等待太长时间
- 若是进程没有sleep或者被I/O阻塞,他们使用CPU的时间是差很少的
进程特性的nice值容许,进程间接的影响内核的调度算法,取值范围是-20(最高)~19(最低),默认0
- 只有特权进程才能赋值给本身或者其余进程一个负的优先级
- 非特权进程只能下降本身的优先级,即赋值一个大于0的nice值
- fork()出建立的子进程会继承nice值,并在exec时保留
- 进程的调度不是严格按照nice值得层次进行的,相反Nice值是一个权重因素
# include<sys/resource.h>
int getpriority(int which, id_t who);
//成功时返回nice值(-20~19),失败时返回-1(和成功值重复)
int setpriority(int which, id_t who, int prio);
//成功0,失败-1
复制代码
who的值取决于which的值
- which = PRIO_PROCESS:操做进程ID为who的进程,who为0时,使用调用者的进程ID
- which = PRIO_PGRP:操做进程组ID为who的进程组中的全部进程,若是who为0,那么使用调用者的进程组
- which = PRIO_USER:操做真实用户ID为who的进程,若是who为0,使用调用者的真实用户ID(不一样unix实现,非特权进程对于真实用户和有效用户的匹配时能够设置权限有区别)
getprioroty
当是多个进程是返回优先级最高的进程的nice值(由于可能返回-1,须要调用前将errno置0)
linux 内核2.6.12开始:
- linux提供了RLIMIT_NICE资源限制,容许非特权进程提高nice值,非特权进程能够将本身提高到
20 - rlim_cur
的值- 非特权进程能够经过
setpriority
来修改其余目标进程的nice值,前提是调用setpriority()
的进程的有效用户ID与目标进程的真实或有效用户ID匹配,而且符合RLIMIT_NICE
限制
实时应用对调度器有更加严格的要求
- 实时应用必需要为外部输入提供担保最大响应时间
- 高优先级进程可以保持互斥的访问CPU直到他完成或自动释放CPU
- 实时应用进程可以精确地控制其组建进程的调度顺序
- SUSv3实时进程调用API提供的策略(同时用SCHED_OTHER标记循环时间分享策略,如下优先级均高于其):
- SCHED_RR
- SCHED_FIFO
- linux 提供了99(1(低)~99(高))个实时优先级,以上两个策略中的优先级是等价的
- 每一个优先级维护者一个可运行队列
- POSIX实时(软实时)与硬实时。和时间分享应用程序有冲突;linux2.6.18以后为硬实时应用程序提供了彻底的支持
- SCHED_RR(循环)策略:优先级相同的进程以循环时间分享的方式执行,每次使用CPU的时间为一个固定长度的时间片,一旦被调度执行以后会保持对CPU的控制直到:
- 达到时间片的终点
- 自愿放弃CPU,多是执行了
sched_yield()
- 终止了
- 被更高优先级的进程抢占了
- 以前被阻塞的高优先级进程解除阻塞了
- 别的进程优先级提升或本身优先级下降
- 前二者会将进程置于其优先级队列队尾,最后一个在抢占进程结束后执行剩余时间片
- 不一样于SCHED_OTHER,SCHED_RR是严格按照优先级来的
- SCHED_FIFO:不一样于SCHED_RR,SCHED_FIFO不存在时间片,被调度执行以后会保持对CPU的控制直到:
- 自愿放弃CPU,多是执行了
sched_yield()
- 终止了
- 被更高优先级的进程抢占了(和SCHED_RR情形同样)
- 第一种状况会将进程置于其优先级队列队尾,最后一个优先级进程结束(终止或者被阻塞)以后,被抢占进程继续执行
- SCHED_BATCH,SCHED_IDLE 略(非标准)
#include<sched.h>
int sched_get_priority_min(int policy);
int sched_get_priority_max(int policy);
// 不一样操做系统min,max值不一样,不必定是1~99
int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);
struct sched_param{
int sched_priority;
}
// 对于linux, SCHED_RR和SCHED_FIFO, sched_priority值必须在min和max之间,其它策略值只能是0
// 成功调用以后会将进程置于队尾
// fork()建立的子进程会继承父进程的调度策略和优先级,并在exec()中保持
int sched_setparam(pid_t pid, const struct sched_param *param);
复制代码
从linux2.6.12开始,引入RLIMIT_RTPRIO,使非特权进程按照一点规则修改CPU调度
- 进程拥有非0
RLIMIT_RTPRIO
软限制时,能够任意修改本身的调度策略和优先级,优先级上限为当前实时优先级的最大值及其RLIMIT_RTPRIO
软限制的约束- 若是
RLIMIT_RTPRIO
为0,进程只能下降优先级或者从实时策略转化为非实时策略- SCHED_IDLE 策略是特殊的策略,此时进程没法修改本身策略
- 在其余非特权进程也能执行策略和优先级的修改,只有有效用户ID是目标进程的真实或有效用户ID
- 防止实时进程锁住系统:略
- 避免子进程进程特权调度策略(避免fork继承):
- 当调用
sched_setscheduler()
时policy传SCHED_RESET_ON_FORK时,由这个进程建立的子进程不会继承特权进程的调度策略和优先级
- 若是调用进程策略是SCHED_RR或SCHED_FIFO,则紫禁城策略会被置为SCHED_OTHER
- 若是nice<0,则置为0
#include<sched.h>
int sched_yield(void);
//在非实时进程调用的结果是未定义的
int sched_rr_get_interval(pid_t pid, struct timespec *tp);
//获取RR策略下的每次被受权CPU时间长度
复制代码
- 进程切换CPU(原来的CPU处于忙碌状态)
- 若是原来CPU的高速缓存保存进程数据,为了将进程的这一行数据加载到新的CPU,首先须要使这行数据失效(没被修改时丢弃,修改时写入内存),(为防止高速缓冲不一致,多处理器架构某一时刻只容许数据被存放在一个CPU的高速缓冲中)
- 为减小以上的性能损耗,Linux2.6以后加入了CPU亲和力
- 亲和力的设置同调度策略
#define _GNU_SOURCE
#include<sched.h>
int sched_setaffinity(pid_t pid, size_t len, cpu_set_t *set);
复制代码