title: linux/unix编程手册-31_35 date: 2018-07-23 11:53:07 categories: programming tags: tips
linux/unix编程手册-31(线程安全和每线程存储)
线程安全:可重入(略)
一次性初始化:
- 库函数中的初始化使用
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在当前进程的存贮
不同线程中的数据缓冲区,线程刚刚创建时会初始化为null
一般流程
- 函数创建一个key,通过传参调用
pthread_key_create()
的函数作为pthread_once()
的参数,调用pthread_once()
保证一次创建key的行为- 通过
pthread_setspecific()
和pthread_getspecific()
来绑定和确认key又没用绑定线程独立缓冲区,没有的话malloc()分配一次,只会有一次。
线程局部存储
static __thread buf[size];
复制代码
__thread
需紧跟extern
或static
后面
linux/unix编程手册-32(线程的取消)
#include<pthread.h>
int pthread_cancel(pthread_t thread);
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
复制代码
设置线程取消状态和类型
- 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);
复制代码
异步取消
- 可异步取消线程不应该分配任何资源,也不能获取互斥量和锁
- 可使用在取消计算密集型的循环的线程
linux/unix编程手册-33(线程:更多细节)
线程栈
- 线程栈的大小可以通过创建线程时的
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,所有线程消失,不会对线程特有数据额结构进行析构,也不会调用清理函数
线程的实现模型(线程和KSE的关系)
- 多对一:线程创建的,调度,同步的所有细节由进程内用户空间的线程库处理(类似携程?)
- 速度快,无需内核态切换
- 移植相对方便
- 当一个线程发起内核调用阻塞时,所有线程阻塞
- 内核感知不到县城,无法调度给不同的CPU,无法调整线程优先级
- 一对一
- 避免了多对一弊端
- 但是维护每一个KSE需要开销,增加内核调度器的负担
- 进程创建切换等操作比多对一慢
- 多对多
- 每个进程拥有多个KSE, 并且可以把多个线程映射到一个KSE,权衡商量中模型
- 但是模型过于复杂,调度由线程库和内核共同实现
线程的具体实现
Linux POSIX的实现
- LinuxThread(旧)
- NPTL(一对一)
linux/unix编程手册-34(进程组,会话和作业控制)
- 进程组是一组相关进程的集合
- 会话是一组相关进程组的集合
进程组
- 一个进程组拥有一个进程组的首进程,该进程是创建这个进程组的进程,其进程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信号
- SIGHUP信号 默认行为是终止进程,若忽略此信号,进程后续从终端读取数据的请求会抛出异常
- 出现在:
- 终端驱动器检测到连接断开
- 终端窗口被关闭:(终端窗口关联的伪终端的主测文件描述符被关闭了)
shell中处理
SIGHUP
信号(关联行为)
nohup
命令可以用来使一个命令对SIGHUP的处置置位SIG_IGNdisown
(bash)会从shell的任务列表删除一个任务,这样其在shell终止时不会收到SIGHUP- shell通常会是终端的控制进程
- shell收到SIGHUP只会发SIGHUP给由它创建的进程组的进程(前台和后台进程)(如果子进程新建了个进程组不会被通知)
SIGHUP和控制进程的终止
exec
会导致shell执行一个exec()
使指定程序替代自己 与shell不同,如果因为终端断开引起的向控制进程发送的SIGHUB信号会导致控制进程终止,那么SIGHUB会发送给终端的前台进程组所有成员
作业控制
fg %1
bg %1
复制代码
%%
,%+
指当前作业%-
指上一次作业
- 后台进程尝试从终端读会受到SIGTTIN信号,停止作业
- 终端设置了TOSTOP命令,后台进程尝试向终端输出时,会收到SIGTTOU信号,停止作业
vim 这个程序在SIGSTP和SIGCONT需要额外的操作保持终端屏幕内容
孤儿进程组
如果一个进程组变成了孤儿进程组中并包含许多被停止进程,SUSv3规定,系统会像进程组中所有成员发送SIGHUB,通知他们和会话断开,再发送SIGCONT确保他们恢复执行
linux/unix编程手册-35(进程优先级和调度)
进程优先级
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 略(非标准)
实时进程调度API
#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,首先需要使这行数据失效(没被修改时丢弃,修改时写入内存),(为防止高速缓冲不一致,多处理器架构某一时刻只允许数据被存放在一个CPU的高速缓冲中)
- 为减少以上的性能损耗,Linux2.6之后加入了CPU亲和力
- 亲和力的设置同调度策略
#define _GNU_SOURCE
#include<sched.h>
int sched_setaffinity(pid_t pid, size_t len, cpu_set_t *set);
复制代码