第二章 线程初始化
前面我们曾经谈到,在OSKit中资源分配和调度的单位是线程而不是传统意义上的进程,所以我们有必要花大量的时间对其线程机制做全面的分析。当然,要分析线程部分,首先应该从线程的初始化入手,然后才能涉及到线程间的通信以及线程调度,所以本章将向各位全面系统地介绍OSKit的线程初始化。
为了让大家能清楚地掌握OSKit的线程部分,在以后的三章内,我将采用理论实际相结合的论述方式,即在每章的开始先论述本章所要讨论的线程机制,以期让大家在概念上对本章有总体上的认识,然后再针对OSKit的源代码做进一步的阐述,希望能理论实践双丰收。
2.1 线程初始化分析
2.1.1线程的创建
在OSKit中,用户创建一个新线程的唯一方法就是调用创建线程的线程pthread_create。
内核为系统调用pthread_create完成如下操作:
(1) 为新线程在线程表项中分配一个空槽。
(2) 赋给子线程一个唯一的标识号
(3) 制作一个父线程上下文的逻辑副本。由于线程的某些部分,如正文区,可能被几个线程所共享,所以内核只要增加某个区的引用计数即可,而不用真的将该区拷贝到一个新的内存物理区。
(4) 增加与该线程相关的文件表和索引节点表的引用数。
(5) 向父线程返回子线程的线程号。
系统对一个用户可以同时运行的线程数有一个限制(这个限制是可以改变的),因而任何用户不能使用过多的线程表项,不然的话,就会妨碍其他用户创建新线程,而且,OSKit为了防止死锁,规定了普通用户不能占用线程表项中的最后一个线程,这条规定是从UNIX中演变而来的。
2.1.2线程的存储
OSKit的线程存储方案与UNIX十分相似,由三个逻辑段组成,它们是正文段、数据段和栈。正文段含有一个线程所执行的指令集合,正文段的地址包括正文地址(用于分支和子程序调用)、数据地址(用于存取全局数据变量)和栈地址(为了存取子程序的数据结构)。
若机器将生成的地址看作物理存储器中的地址,那么,两个线程在它们所生成的地址集合重叠了的情况下,是不可能并发执行的。虽然编译器可以产生在程序之间不重叠的地址,但这样的程序对于通用计算机是行不通的,因为一种机器上的存储容量是有限的,而所有可能被编译的程序集合是无限的。采取此种方法可以在很大程度上避免数据的丢失。
2.1.3线程的数据结构
以下我列出了OSKit中线程的完整的数据结构,其中大部分是从UNIX中继承来的,但也有一些是OSKit自己的,例如,为了制作例子程序所提供的CPU资源分配的部分。
为了使各位一目了然,我对此数据结构做了全部注释,并按照其不同应用领域划分成了几个部分,分别加在了下面的注释之中。
queue_chain_t runq; /* 释放队列链 */
pcb_t *ppcb; /* 指向PCB的指针 */
oskit_u32_t pstk; /* 指向所分配的堆栈的指针*/
void *(*func)(void *); /* 指向执行函数的指针 */
size_t ssize; /* 所分配的堆栈的大小*/
size_t guardsize; /* 警戒堆栈的大小 */
pthread_t tid; /* 线程号 */
oskit_u32_t flags; /* 线程状态标志 */
pthread_lock_t lock; /* 锁 */
queue_chain_t chain; /* 队列链 */
int preempt; /* 线程优先级 */
/*以下用于对死锁的处理*/
pthread_mutex_t mutex; /* 互斥位(死锁保护) */
pthread_cond_t cond; /* 等待死锁标志=1 */
int dead; /* 死锁了 */
/*例子所用到的资源*/
int cputime; /* 占用CPU的时间 */
int cpticks; /* 上一秒所占的节拍数 */
oskit_u32_t pctcpu; /* CPU的占用率 */
int childtime; /* 子线程所占用的CPU的开销 */
/*发送信息所用的变量*/
pthread_lock_t waitlock; /* 等待锁*/
oskit_u32_t waitflags; /* 等待标志位 */
pthread_cond_t *waitcond; /* 等待条件变量 */
struct osenv_sleeprec *sleeprec; /*一个osenv_sleep中的线程 */
/*以下是进程通讯要用到的*/
void *msg; /* 指向要发送的消息的指针 */
oskit_size_t msg_size; /* 消息的大小 */
pthread_t tid; /* 线程号 */
void *reply; /* 指向响应信息的指针 */
oskit_size_t reply_size; /* 响应内容的大小 */
queue_head_t senders; /* 发送队列中的发送者 */
queue_chain_t senders_chain; /* 发送队列指针 */
/* 以下是所用到的时钟 */
struct oskit_timer *condtimer;
/* 线程的休眠和唤醒是用一个单独的计时器实现的 */
struct oskit_timer *sleeptimer;
/* 以下是所用到的信号 */
pthread_lock_t siglock; /* 保护锁信号 */
sigset_t sigmask; /* 阻塞信号 */
sigset_t sigpending; /* 未决信号 */
sigset_t sigwaiting; /* 等待信号的信号 */
oskit_u32_t eip; /* 页处理故障信号 */
/* 键值通常是一个固定的队列 */
void *keyvalues[PTHREAD_KEYS_MAX]; /* 键值 */
/* 以下用于清除操作 */
pthread_cleanup_t *cleanups; /* 清楚操作链 */
char cancelstate; /* 取消状态 */
char canceltype; /* 取消类型 */
/* 以下是单独用于调度的锁 */
pthread_lock_t schedlock;
void *rtai_priv;
int policy; /* 调度策略 */
int priority; /* 当前的优先级 */
struct scheduler_entry *scheduler; /* 调度程序入口 */
int policy; /* 调度策略 */
int priority; /* 当前优先级 */
int base_priority; /* 初始优先级 */
int ticks; /* 调度所省下的节拍 */
oskit_timespec_t start; /* 下次运行的时间 */
oskit_timespec_t deadline; /* 下次运行的时间 */
oskit_timespec_t period; /* 两次执行见的间隔 */
queue_head_t waiters; /* 正在等待的线程 */
queue_chain_t waiters_chain; /* 线程队列链 */
struct pthread_thread *waiting_for; /* 所等待的线程 */
struct pthread_thread *inherits_from; /* 线程从何继承 */
/*以下是CPU的继承*/
struct pthread_thread *scheduler; /* 线程调度程序人口 */
schedmsg_t unblockmsg;
schedmsg_queue_t *msgqueue;
sched_wakecond_t wakeup_cond; /* 唤醒条件 */
schedflags_t schedflags; /* 标志 */
int donate_rc; /* 从捐赠线程返回的值 */
int timeout; /* 毫秒 */
queue_head_t donors; /* 捐赠资源的线程 */
queue_chain_t donors_chain; /* 捐赠队列链 */
struct pthread_thread *donating_to; /* 被捐赠的线程 */
struct pthread_thread *inherits_from; /* 线程从何继承 */
struct pthread_thread *nextup; /* 下一个要执行的线程 */
2.2 pthreads/pthread_create.c
此源码文件包括了一套完整的线程创建机制,它是全部线程的根源所在,通过阅读下面的函数分析,将使读者了解OSKit到底是用什么函数来具体实现线程创建的。我认为在上一节的理论指导下对代码进行分析,要比泛泛的阐述理论更容易让读者接受,更能加深在读者脑海中的印象。
2.2.1 创建线程
说明:这就是我们在第一节中提到的创建线程的函数,所有由用户完成的创建线程的操作,都要调用它来实现。
int pthread_create ( pthread_t *tid, const pthread_attr_t *attr,
void * ( *function )( void * ), void *argument )
tid: 指向线程存储位置的指针
attr: 指向线程属性的指针
*(*function)(void *): 当线程初始化时所调用的函数
*argument: 函数功能
2.2.2 创建内部线程
说明:该函数用于系统核心部分创建核心线程,而一般的用户没有调用它的权利,只有系统核心才可以调用。
pthread_thread_t *pthread_create_internal ( void * ( *function)( void *), void *argument,const pthread_attr_t *attr )
注:内部线程创建时,将阻塞所有的信号
sigfillset(&pthread->sigmask)
2.2.3 为主进程创建一个备份线程
说明:出于系统安全的原因,OSKit定义了此函数,用于对主线程进行备份。由于线程创建子线程,子线程又创建自己的子线程,如此下去,将产生一个以主线程为根节点的树形结构,所以一旦主线程丢失,将有可能导致系统崩溃,所以应对其进行备份。
pthread_thread_t *pthread_init_mainthread ( pthread_thread_t *pthread )
2.2.4 初始化创建线程的线程
说明:这和UNIX中那个创建进程的进程有些类似,它唯一的工作就是为主线程创建子线程,并且,每个线程都包含一个该线程。
pthread_thread_t *thread_init_mainthread ( pthread_thread_t *pthread )
2.2.5 为等待和休眠的线程创建一个等待时间
说明:由于调度的原因,一般线程不可能一直占用CPU,也不可能永远被挂起,所以OSKit为规定线程的等待时间定义了此函数。
void pthread_prepare_timer ( pthread_thread_t *pthread )
2.3 pthreads/pthread_attr.c
此源码文件包括了一套完整的线程属性的初始化机制,它与上一节的线程创建共同作用,规定了线程在创建之初的属性,通过阅读下面的函数分析,将使读者了解OSKit到底是用什么函数来具体实现线程属性的初始化。
2.3.1线程初始化的数据结构
说明:系统在创建新线程的时候,会调用此函数来规定被创建线程的属性,所以此函数在线程属性机制中站有最主要的地位,是一切线程属性函数调用的源泉,希望读者给以充分的重视。
int pthread_attr_init ( pthread_attr_t *attr )
attr->detachstate = PTHREAD_CREATE_JOINABLE; /* 线程与其他线程关联*/
attr->priority = PRIORITY_NORMAL; /* 线程的优先级 */
attr->stacksize = PTHREAD_STACK_MIN; /* 线程所占堆栈的 */
attr->guardsize = DEFAULT_STACKGUARD; /* 警戒堆栈的大小 */
attr->stackaddr = 0; /* 堆栈的大小 */
attr->policy = SCHED_RR; /* 线程调度类型 */
attr->inherit = PTHREAD_EXPLICIT_SCHED; /* 线程继承类型 */
注: memset 的数据结构 /* \oskit\libc\string\memset.c */
void *memset(void *tov, int c, size_t len)
{ register char *to = tov;
while (len-- > 0)
*to++ = c;
return tov;
}
tov: 指向内存的首地址 c: 分配类型 len: 分配长度
2.3.2线程属性的撤销
说明:这个函数是上一个函数的逆操作,在撤销一个线程的同时,其属性也应相应地被撤销,所以OSKit定义了此函数来实现该功能。
int pthread_attr_destroy ( pthread_attr_t *attr )
memset((void *) attr, -1, sizeof(*attr)); /* 撤销指针并释放内存*/
2.3.3设置线程的警戒堆栈大小
说明:警戒堆栈在操作系统中被广泛使用,它是源于UNIX的进程间通信,在UNIX系统中,接受进程的消息缓冲可能小于发送进程。所以,当消息到来的时候,为了避免溢出造成的信息丢失,系统创建了警戒堆栈,用来存储溢出的消息。而OSKit把它用于线程间通信的保护,但用法与UNIX大同小异。
int pthread_attr_setguardsize ( pthread_attr_t *attr, size_t guardsize )
2.3.4得到警戒堆栈的大小
说明:由于OSKit为每个接受线程设置警戒堆栈,所以定义此函数,为的是系统能方便的得到每个接受线程的警戒堆栈的大小,这同样是出于对安全的考虑。
int pthread_attr_getguardsize ( const pthread_attr_t *attr, size_t *guardsize )
2.3.5设置线程的分离状态
说明:上边曾经提到,系统中的线程树,而线程与线程之间很可能并不是完全独立的,即使他们处于不同的层次,也可能有一定的相关性或互斥性,所以定义此函数用来规定线程之间的互斥性,即分离状态是完全有必要的。
int pthread_attr_setdetachstate( pthread_attr_t *attr, int detachstate )
2.3.6 得到线程的分离状态
说明:在规定了分离状态之后,若A线程要与B线程发生关联的时候,它必须先看看B线程和其他线程的关系,所以要定义此函数来得到B线程与其他线程的分离状态,提供给A,然后A在根据具体情况决定是否与B发生关系。
int pthread_attr_getdetachstate ( const pthread_attr_t *attr, int *detachstate )
*detachstate = attr->detachstate;
2.3.7 设置线程的继承关系
说明:当线程创建子线程的时候,上边我提到的那个创建线程的线程会为他们父子之间规定继承关系,即子线程继承父线程。
int pthread_attr_setinheritsched ( pthread_attr_t *attr, int inheritstate )
attr->inherit = inheritstate;
2.3.8 得到线程的继承状态
说明:当系统要在复杂的线程树中得到继承关系的时,调用此函数便能达到预期的目的。OSKit定义此函数,为的是能在复杂的线程树中一眼看穿某两个线程之间的关系。
int pthread_attr_getinheritsched ( const pthread_attr_t *attr, int *inheritstate )
*inheritstate = attr->inherit;
2.3.9 设置线程的调度参数
说明:OSKit提供了多种线程调度方式供选择,而且还为每个线程在创建的时候规定了一种调度算法,系统在创建线程的时候调用下面的函数来完成这项工作。
int pthread_attr_setschedparam ( pthread_attr_t *attr,const struct sched_param *param )
int pri = param->priority;
attr->start = param->start; /* 线程开始参数 */
attr->period = param->period; /* 线程的周期 */
attr->deadline = param->deadline; /* 线程运行的最终时限 */
2.3.10 得到线程调度的参数
说明:在OSKit中,每个线程都有其自身的调度方式,换句话说,就是线程的调度方式仿佛是线程的一个属性,在创建线程的时候就随线程被规定了,所以调度程序可以通过下面的函数来得到某个线程的调度算法和此时它的优先级。
int pthread_attr_getschedparam ( pthread_attr_t *attr, struct sched_param *param )
param->priority = attr->priority;
2.3.11 设置线程调度策略
说明:在某些方面,线程的调度策略的确很像线程的属性,但它却是可以在调度中被更改的,系统可以调用此函数完成对线程调度策略的重定义。这样的规定,使得线程调度灵活了许多,当然这种灵活是用增加系统开销的代价换回来的,但却是值得的。
int pthread_attr_setschedpolicy ( pthread_attr_t *attr, int policy )
switch (policy) {
case SCHED_FIFO: /* 先到先处理 */
case SCHED_RR: /* 时间片轮转 */
case SCHED_EDF /* 先死先处理 */
case SCHED_RMS: /* 效率优先 */
attr->policy = policy;
break;
case SCHED_DECAY:
printf("pthread_attr_setschedpolicy: "
"SCHED_DECAY not supported yet\n");
}
2.3.12 得到线程调度策略
说明:负责调度的线程通过该函数得到被调度线程的调度策略,然后再根据此时系统内的情况,决定是否改变其调度算法。
int pthread_attr_getschedpolicy ( const pthread_attr_t *attr, int *policy )
*policy = attr->policy;
2.3.13 设置堆栈地址
说明:调用此函数,可以实现对线程属性堆栈的底层操作,即设置指向该堆栈的指针,并保存起来,为以后的操作奠定基础。
int pthread_attr_setstackaddr ( pthread_attr_t *attr, oskit_u32_t stackaddr )
attr->stackaddr = stackaddr;
2.3.14 得到堆栈的地址
说明:返回指向线程属性堆栈的指针。
int pthread_attr_getstackaddr(const pthread_attr_t *attr, oskit_u32_t *stackaddr)
*stackaddr = attr->stackaddr;
2.3.15 设置堆栈的大小
说明:由于系统中的线程功能、大小各不相同,所以其属性堆栈的大小也不尽相同,调用此函数可以设置线程属性堆栈大小。
int pthread_attr_setstacksize ( pthread_attr_t *attr, size_t stacksize )
attr->stacksize = stacksize;
2.3.16 得到堆栈的大小
说明:返回某个线程堆栈的大小。
int pthread_attr_getstacksize ( const pthread_attr_t *attr, size_t *stacksize )
*stacksize = attr->stacksize;
2.3.17设置线程的优先级
说明:在操作系统的线程调度中,优先级调度的使用最为广泛,OSKit定义了下面的函数来规定线程的优先级。
int oskit_pthread_attr_setprio ( pthread_attr_t *attr, int pri )
attr->priority = pri;
2.3.18 准备接受调度
说明:由于多态性原因,不是所有的线程都要参与调度,只是某些线程参加,所以,系统必须调用此函数,告诉该线程准备参加调度,而被调度的方式在上边的几个函数里已经规定好了。
int pthread_attr_setopaque ( pthread_attr_t *attr, oskit_u32_t opaque )
attr->opaque = opaque;
2.3.19 声明要进行调度的线程
说明:调用此函数是为了告诉系统,哪些线程是要被调度的。
int pthread_attr_setscheduler ( pthread_attr_t *attr, pthread_t tid )
attr->scheduler = tid;
2.3.20 设置mutex的属性
说明:在OSKit中,广泛使用了mutex,主要是为了更好的使用临界资源,有效地避免了死锁,换句话说,mutex就像临界资源的数量一样,线程对其进行PV操作。而当一个线程占有了一个mutex,并且在没释放它之前该线程又创建了子线程,此时,子线程理所当然地从父线程手中继承了mutex的所有权,OSKit定义了下面的函数来实现所叙述的功能。
int pthread_mutexattr_init ( pthread_mutexattr_t *attr )
attr->priority_inherit = PTHREAD_PRIO_NONE; /* THREAD_PRIO_NONE=0 */
2.3.21撤销mutex的属性
说明:当占有mutex的线程放弃了它的时候,调用该函数来撤销mutex的属性。
int pthread_mutexattr_destroy ( pthread_mutexattr_t *attr )
memset((void *) attr, -1, sizeof(*attr));
2.3.22 设置muxtex的协议
说明:mutex的使用并不是简单的占用与撤销,OSKit还提供了更加完善的mutex协议,它根据inherit的不同,将mutex多样化。
int thread_mutexattr_setprotocol ( pthread_mutexattr_t *attr, int inherit )
switch (inherit) {
case PTHREAD_PRIO_NONE:
case PTHREAD_PRIO_INHERIT:
attr->priority_inherit = inherit;
break;
default:
return EINVAL;
}
注:在线程调度时,根据mutex协议产生mutex变量,将线程按照优先级排队
2.3.23 得到线程调度的mutex协议
说明:好像对协议的解析一样,调用此函数,可以返回该线程的mutex。
int pthread_mutexattr_getprotocol ( const pthread_mutexattr_t *attr, int *inherit)
*inherit = attr->priority_inherit;
2.3.24 设置mutex的种类
说明:mutex在OSKit中是可以多种多样的,通过调用下面的函数,可以规定线程到底占用了哪种mutex。
int pthread_mutexattr_settype ( pthread_mutexattr_t *attr, int type )
switch (type) {
case PTHREAD_MUTEX_NORMAL: /* 普通 */
case PTHREAD_MUTEX_ERRORCHECK: /* 错误检查 */
case PTHREAD_MUTEX_RECURSIVE: /* 递归 */
case PTHREAD_MUTEX_DEFAULT: /* 默认 */
attr->type = type;
break;
default:
return EINVAL;
}
2.3.25得到mutex的类型
说明:当系统想要知道某个线程占用的是什么类型的mutex时,调用此函数来返回mutex的类型。
int pthread_mutexattr_gettype ( const pthread_mutexattr_t *attr, int *type )
*type = attr->type;
本 章 小 结
本章从线程的创建、存储和数据结构到各种属性以及对它们的操作(设置、撤销和得到)入手,系统地阐述了OSKit线程机制的根源--线程初始化。
在第一节主要从三个角度归纳了OSKit中线程所涉及的概念,它们是创建、存储以及线程的数据结构。第二节中以源代码中的 /pthreads/pthread_creat.c 为例,说明了第一节中概念的具体实现。第三节以源代码中的 /pthreads/pthread_attr.c 为例,说明了Oskit对线程属性的全部操作,包括线程初始化的数据结构、属性的撤销、线程间关系、对mutex的操作以及对属性堆栈的操作等。