linux线程栈与进程栈

1 线程使用方法

pthread_create用于创建一个线程,pthread_join用于等待线程执行完毕,简单应用如下:

#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>

void tid_1(){
    printf("I am tid 1!\n");
    sleep(1);
    return;
}

void tid_2(){
    printf("I am tid 2!\n");
    sleep(2);
    return;
}

int main(int argc, char *argv[]){
    pthread_t tid1, tid2;
    if(pthread_create(&tid1, NULL, (void *)&tid_1, NULL) || pthread_create(&tid2, NULL, (void *)&tid_2, NULL)){
        printf("pthread_create error!\n");
        exit(-1);
    }
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

编译执行后的结果如下:
这里写图片描述
那么问题出现了,在操作系统中,线程是执行的最小单位,进程是资源分配的最小单位,而栈资源是内存资源的一种,按理说线程应该共享进程的栈资源,但是如果线程共享进程的栈资源的话,那就不可能出现多个线程可以并行运行的状态,因为栈是用来存储函数中参数、局部变量、返回地址等的地方,这种内存资源不可能被并行程序共享,所以,很明显,线程不共享进程的栈资源,线程栈和进程栈是分开的,而且每个线程都拥有一个独立的栈空间,那么线程栈和进程栈之间的关系又是什么呢?

2 用户空间的线程栈资源的管理

2.1 其实,pthread_*函数是glibc库对于栈的一种实现的封装,主要的函数和参数如下:

Linux下的多线程遵从POSIX线程接口,简称pthread,在pthread库中提供。
pthread_create():创建一个线程
pthread_exit():退出一个线程
pthread_jion():阻塞当前线程,直到另一个线程执行结束
pthread_attr_init():设置线程是否脱离属性
pthread_attr_setstack(&tattr,stack,16*1024*1024); //分配线程的栈空间
pthread_kill():给线程发送kill信号
int pthread_cancel(pthread_t thread);//发送终止信号给thread线程,如果成功则返回0
int pthread_setcancelstate(int state, int *oldstate);//设置本线程对Cancel信号的反应
int pthread_setcanceltype(int type, int *oldtype);//设置本线程取消动作的执行时机
void pthread_testcancel(void);//检查本线程是否处于Canceld状态,如果是,则进行取消动作,否则直接返回
同步函数:
pthread_mutex_lock():互斥加锁
pthread_mutex_unlock():互斥锁解锁
pthread_cond_init():初始化条件变量
pthread_cond_signal():发送信号唤醒进程
pthread_cond_wait():等待条件变量的特殊事件发生
(备注:参考自https://www.cnblogs.com/minihaohao/p/5188906.htmlhttps://blog.csdn.net/liujiabin076/article/details/53456962

2.2 glibc对于pthread_create函数的实现

源码文件./nptl/pthread_create.c中,__pthread_create_2_1 (newthread, attr, start_routine, arg)便是对pthread_create函数的定义位置。
传入的参数如下:
pthread_t *newthread;
const pthread_attr_t *attr;
void (*start_routine) (void );
void *arg;

pthread_t的定义为typedef unsigned long int pthread_t,是线程的标识id
pthread_attr_t的定义如下

union pthread_attr_t
{
char __size[__SIZEOF_PTHREAD_ATTR_T];
long int __align;
};

实质上是一个字符串,但是其对应的真实结构体的样子如下所示:

struct pthread_attr
{
/* 调度参数和优先级别设置. */
struct sched_param schedparam;
int schedpolicy;
/* 不同的标识,比如 detachstate, scope等. */
int flags;
/* Size of guard area. */
size_t guardsize;
/* 栈资源的地址和大小 */
void *stackaddr;
size_t stacksize;
/* Affinity map. */
cpu_set_t *cpuset;
size_t cpusetsize;
};

pthread_create对于线程栈资源的操作过程解析如下:

2.2.1 pthread_create使用函数nt err = ALLOCATE_STACK (iattr, &pd)给线程创建栈资源,ALLOCATE_STACK的宏定义为:

#define ALLOCATE_STACK(attr, pd) allocate_stack (attr, pd, &stackaddr)
然后,我们进入函数allocate_stack继续分析,首先设置栈资源的大小(如果指定了就使用传进来的参数,否则使用默认值):

if (attr->stacksize != 0)
size = attr->stacksize;
else
{
lll_lock (__default_pthread_attr_lock, LLL_PRIVATE);
size = __default_pthread_attr.stacksize;
lll_unlock (__default_pthread_attr_lock, LLL_PRIVATE);
}

接下来,如果用户提供了栈资源的内存空间,则对该空间资源进行大小、有效性检查、清空操作等;然而,如果用户没有指定栈资源空间,则为该线程分配一些匿名内存或者从cache中获取内存,设置内存标识prot为PROT_READ | PROT_WRITE,调整栈大小size为页对齐或者cache大小对齐,然后调用函数mmap分配内存如下:

mem = mmap (NULL, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);

接下来对获取的内存进行相应地检查,最后赋值给相应变量:pd->stackblock = mem; pd->stackblock_size = size。

2.2.2 根据设置的相关标识依次调用系统调用sys_sched_getscheduler、sys_sched_getparam、sys_sched_get_priority_min、sys_sched_get_priority_max,最后真正的执行线程创建和调度:retval = create_thread (pd, iattr, STACK_VARIABLES_ARGS); create_thread()函数的执行流程如下:

create_thread (struct pthread *pd, const struct pthread_attr *attr, STACK_VARIABLES_PARMS)
设置clone标识位:

int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAL
| CLONE_SETTLS | CLONE_PARENT_SETTID
| CLONE_CHILD_CLEARTID | CLONE_SYSVSEM
| 0);

然后调用sys_clone系统调用创建线程:

int res = do_clone (pd, attr, clone_flags, start_thread, STACK_VARIABLES_ARGS, stopped);

3 内核中线程的实现

linux内核中没有明确的栈的概念,不过Linux内核中的进程支持灵活的创建模式,于是可以利用轻量级进程、线程组资源、各种其他资源的共享来用进程模拟线程,而且可以借助于进程的调度算法来实现线程的调度问题。
具体的内核中关于线程的创建,具体请根据上述clone_flag来阅读sys_clone的代码实现,实际上,sys_clone的代码实现主要集中在函数do_fork()的实现中。

4 总结

综上述所,一个进程的所有线程所使用的栈都是进程原本的资源,其资源主要有两种创建方法:用户显式创建、glibc默认创建,根据这两种创建方法可以得知:线程使用的栈资源主要来自于进程的三个内存区域—栈区(注意,使用进程栈分配线程栈资源导致不可预料的问题,一般都是大小问题,不过因为栈是一种限定在函数内部的临时资源,函数退出栈资源便会消失,于是也可能出现其他不可预料的状况。)、堆区、mmap匿名映射区。

猜你喜欢

转载自blog.csdn.net/u011414616/article/details/80952210