Linux的线程实现是在内核以外来实现的,内核本身并不提供线程创建。但是内核为提供线程【也就是轻量级进程】提供了两个系统调用__clone()和fork
(),这两个系统调用都为准备一些参数,最终都用不同的参数调用do_fork()核内API。do_fork()提供了很多参数,包括CLONE_VM(共享内存空间)、CLONE_FS(共享文件系统信息)、CLONE_FILES(共享文件描述符表)、CLONE_SIGHAND(共享信号句柄表)和CLONE_PID(共享进程ID,仅对核内进程,即0号进程有效)。当直接使用fork系统调用时,内核调用do_fork()不使用任何共享属性,进程拥有独立的运行环境,而使用pthread_create()来创建线程时,则最终设置了所有这些属性来调用__clone(),而这些参数又全部传给核内的do_fork(),从而创建的"进程"拥有共享的运行环境,只有栈是独立的,由
__clone()传入。
我们知道Linux线程在核内是以轻量级进程的形式存在的,拥有独立的进程表项,而所有的创建、同步、删除等操作都在内核以外的pthread库中进行。pthread
库使用一个管理线程(__pthread_manager(),每个进程独立且唯一)来管理线程的创建和终止,为线程分配线程ID,发送线程相关的信号(比如Cancel),而主线程(pthread_create())的调用者则通过管道将请求信息传给管理线程。
上面一段话,一般的内核介绍书籍都会有说到,在开篇之前,我想先说明一下内核关于进程的主线程设计想法:
每个进程都有自己的PID,这是一定唯一的,除此之外,每个进程还有其它的ID标识,比如处于某个线程组中的所有进程都有一个统一的线程组ID(TGID),如果进程没有使用线程,PID和TGID是相同的,其实主线程的PID和TGID就是相同的。另外,独立进程可以合并成为进程组。还有几个进程组可以合并成一个会话,会话中的所有进程都有同样的会话ID。和我们这里讨论相关的主要是task_struct的两个成员
struct task_struct{
pid_t pid;
pid_t tgid;
}
位于同一个进程的所有线程,tgid是相同的,都指向“线程”组第一个进程的PID值,这也就是主线程。可以分别通过getpid(), syscall(SYS_gettid)获取其值。
一,创建线程
在创建进程时,操作系统会为其创建一个主线程,之后由这个线程创建的线程都是从线程,创建主线程的方法如下:
int pthread_create(pthread_t *threadId,pthread_attr_t *attr, void*(start_routine)(void*),void *args).
参数解释如下:
threadId : 用于返回创建的线程的ID;每个线程都有自己的ID,在线程内可以调用pthread_self()函数获取ID值,该函数原型是这样的:pthread_t pthread_self()。
arr : 用于指定将要被创建的线程的属性;该值可以为NULL,表示默认的属性。等下专门说下这个属性,暂时知道这个参数表示用于创建线程的方式就可以了;
start_routine : 这是一个函数的指针,指定线程被调度时的入口;
args : 用于线程入口函数的参数;
如下调用就可以了:
先定义一个入口函数:
- <span style="font-size:18px;">void * start_thread(void *){
- printf("this will be printed by the new thread");
- }</span>
那么创建 一个线程的代码可以如下,当然这很简单,实际过程中不可能是这样的:
- <span style="font-size:18px;">int main(int argc,char **argv){
- pthread_t threadId;
- pthread_create(&threadId,NULL,start_thread,NULL);
- }</span>
注意传递的参数:线程返回ID为引用类型,线程创建属性和参数都是NULL;线程入口地址为start_thread所指向的地址;
二,线程属性
线程属性由数据结构pthread_attr_t结构表示,其定义如下所示:
- <span style="font-size:18px;"> typedef struct
- {
- int detachstate; 线程的分离状态
- int schedpolicy; 线程调度策略
- struct sched_param schedparam; 线程的调度参数
- int inheritsched; 线程的继承性
- int scope; 线程的作用域
- size_t guardsize; 线程栈末尾的警戒缓冲区大小
- int stackaddr_set;
- void * stackaddr; 线程栈的位置
- size_t stacksize; 线程栈的大小
- }pthread_attr_t;</span>
这个结构体在使用过程中由pthread_attr_init和pthread_attr_destory负责数据的初始化和销毁;
detachstate:
线程的分离状态表示线程会以什么样的方式来终止自己,或者说该状态用来表示创建的线程是否保持与进程中其他的线程一种同步。如果这个没有被置位,那就不能用pthread_join调用来同步,当线程终止时,自行释放所占用的资源,而不用在pthread_join返回之后,再来释放。该值默认的情况是PTHREAD_CREATE_JOINABLE状态,在线程创建之后,可以调用pthread_detach()来设置为PTHREAD_CREATE_DETACH状态。一旦设置了这个状态创建线程之后,将不能被重新设为PTHREAD_CREATE_JOINABLE
状态;
schepolicy : 表示线程被调度的策略。主要包括SCHED_OTHER(正常、非实时)、SCHED_RR(实时、轮转法)和 SCHED_FIFO(实时、先入先出)三种,缺省为SCHED_OTHER,后两种调度策略仅对超级用户有效。运行时可以用过 pthread_setschedparam()来改变,该函数原型是这样的:int pthread_setschedparam(pthread_t target_thread, int policy, const struct sched_param *param)。这个调用可以动态的改变调度策略和线程的优先级。
scheparam: 一个struct sched_param结构,结构体包含一个sched_priority整型变量表示线程的运行优先级。这个参数仅当调度策略为实时(即SCHED_RR 或SCHED_FIFO)时才有效,并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0。
inheritshed : 表示线程创建的继承;调用函数pthread_attr_setinheritsched和pthread_attr_getinheritsched用来设置和得到线程的继承属性。有两种值可供选择:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新线程使用显式指定调度策略和调度参数(即attr中的值),而后者表示继承调用者线程的值。缺省为PTHREAD_EXPLICIT_SCHED。
scope : 表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值: PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后者表示仅与同进程中的线程竞争CPU。可以通过调用pthread_attr_getscope(), pthread_attr_setscope(),用于获取属性中的线程的作用域。
stacksize,stackaddr : 表示线程堆栈的大小和地址。也有四个函数用于设置和获取相应的值。pthread_attr_getstatcksize(),pthread_attr_setstatcksize(),pthread_attr_setstackaddr(),pthread_attr_getstatckaddr()。
guardsize : 控制着线程栈末尾之后以避免栈溢出的扩展内存大小。同样可以调用pthread_attr_setguardsize()和pthread_attr_getguardsize()。