Linux线程(一)

地址空间回顾

如何看待地址空间和页表:

1.地址空间是进程能看到的资源窗口。

2.页表决定,进程真正拥有的资源情况。

3.合理的对地址空间+页表进行资源划分,就可以对一个进程的所有资源进行分类,划分以及快速访问。

重新认识虚拟地址空间和页表:

需要明确的一点是:当程序从磁盘加载到内存的时候,已经划分成一个个4KB大小的空间,然后会以4KB为单位加载到实际内存中。

 从图上可知:页表不是单纯的一张页表,分为页目录+页表,其中页目录中记录的是虚拟地址空间的前10位的地址,所以页目录的大小为2^10bit。这前10位的地址决定了虚拟地址空间中间10位所在后续页表的那一页,后续页表的页数是由虚拟空间大小决定的,中间10位用来索引实际地址空间中所对应的变量存储的真实地址。索引方法为指定页框的起始物理地址+虚拟地址的低12位作为页内偏移。这样就完成了从虚拟地址到物理地址的映射。

Linux线程概念

在了解线程之前,首先回顾一下进程,进程=内核数据结构+进程对应的代码和数据,从内核角度来看进程是承担分配系统资源的基本实体,他是PCB+虚拟内存+页表+加载到内存的数据和代码的总和。而如何看待虚拟内存呢,虚拟内存决定了进程能够看到的“资源”。

所以什么是线程呢?线程是进程内的一个执行流,线程在进程内部运行,线程在进程的地址空间内运行,拥有该进程的一部分资源。而进程内部可以有多个执行流,每个执行流都指向了虚拟内存这一段空间,进程的资源可以通过虚拟地址空间+页表的方式把自己部分资源分给特定的线程,所以有了线程的概念之后,现在CPU每次执行进程的时候,其实是执行的是一个个线程,也被叫做轻量级进程。

相较于多进程而言:多线程没有多创造一份虚拟内存和页表,只是多了几份PCB,并且这几份PCB指向的是同一块儿虚拟内存。

对于Linux系统而言,OS管理线程的时候,不需要重新设计结构对线程进行管理,因为线程和之前所设计的进程有很多地方都是重叠的(id,状态,优先级,上文信息,栈...)。所以线程这一块儿的管理直接复用了PCB,用PCB来表示“线程”。

总结:

1、Linux内核中没有真正意义的线程,使用进程PCB模拟线程,是一种完全属于自己的线程方案。

2、站在CPU的角度,每一个PCB(线程),都可以称之为轻量级进程。

3、Linux线程是CPU调度的基本单位,线程从进程处获取对应线程的资源。

4、Linux中没有正在意义的线程。(Linux无法直接提供创建线程的系统调度接口,而只是提供了轻量级进程的接口)。

5、Linux线程是CPU的基本调度单位,而进程是承担分配系统资源的基本单位。

对于第四点还可以在编译代码的时候找到,在g++的后面必须带上编译选项-lpthread才能编译通过。必须要链接动静态库才能狗编译通过,Linux无法直接提供创建线程的系统调度接口,而只是提供了轻量级进程的接口。

创建线程代码的函数:

       #include <pthread.h>
       int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

创建线程的代码:

#include <iostream>
#include <stdio.h>
#include <cassert>
#include <unistd.h>
#include <pthread.h>
using namespace std;

int num =0;

void *myfunc(void* args)
{
    while(true)
    {
        cout<<"我是新线程  "<<"num= "<<num++<<"  &num="<<&num<<endl;
        sleep(1);
    }
}
int main()
{
    pthread_t ptd;
    int n = pthread_create(&ptd, NULL, myfunc, (void*)"pthread one");  //创建一个新线程,将资源分配给他
    assert(n==0);  
    while(true)
    {
        char buffersize[64];
        snprintf(buffersize,64,"0x%x",ptd);
        cout<<"我是主线程 "<<" num= "<<num<<" &num="<<&num<<" ptd=   "<<ptd<<endl;
        sleep(1);
    }
    return 0;
}

查看创建的线程,可以看到线程的PID是一样的,而LWP(light weight process)是不一样的(LWP和PID一样的线程是主线程),这是线程的唯一标识符。所以CPU调度的时候,是以LWP进行调度的。

 从上诉代码可以知道:

1、线程一旦被创建,几乎所有的资源都是被线程所共享的。

2、线程一定有自己的私有资源:①PCB属性私有(id,状态,优先级等);②有自己的上下文结构,也就是当线程运行的时候,一定有一组寄存器将他运算过程中产生的临时变量保存,如果被线程被切换,是需要将这些数据保存起来的,这也是动态运行的属性。③每一个线程都有自己独立的栈结构,用来保存自己线程内部的临时变量,或者ebp,esp等。

3、与进程切换相比,线程之间的切换需要OS做的工作要少很多①进程切换:切换页表 && 虚拟地址空间 && 切换PCB && 上下文切换 ;②线程切换:切换PCB && 上下文切换。并且对于线程切换而言,cache的数据不用更新,但是进程切换的话,全部更新。

补充cache:cache是CPU内部的硬件级缓存,表示为L1,L2,L3三个级别的快速寄存器,其实CPU执行命令的时候,不是直接从内存中获取数据或者指令,是先将内存的部分要使用到的数据预先放入这些缓存之中,然后CPU在从这些缓存中拿取数据,进行运算执行等操作。对于代码而言,你的代码变量地址连贯性越高,高速缓存的命中率越高,也就是在需要运行的数据的时候,顺带着把一会儿要用的数据也拿进来了,而对于那些不连续的数据结构,链表之类的,地址不连续,相差很远,就需要多次从内存中再次拿取数据,高速缓存命中率较低。

 Linux多线程

1、线程的创建

//线程创建的函数
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
//第一个参数:输出型参数
//第二个参数:nullptr
//第三个参数:一般设置为要线程执行的函数
//第四个参数:传递给第三个变量的参数,类型为void*
    vector<Thread_Data*>  td;
    //创建10个子线程
    for(int i=0;i<Num;++i)
    {
        Thread_Data* pid = new Thread_Data();   //在堆上创建这些数据
        snprintf(pid->namebuffer,sizeof(pid->namebuffer),"%s:%d","threadnum",i+1);
        pid->num=i+1;
        pthread_create(&pid->id,nullptr,stat_routine,(void*)pid);   
 //这是系统级别的调用接口,还有vfork也是创建进程的,还有clone函数也是创建进程的
        td.push_back(pid);
    }

 创建的线程执行顺序是不一定的,这个是随机的看OS的,不一定先创建的线程先执行。

2、线程的退出

#include <pthread.h>
void pthread_exit(void *retval);  //线程退出函数
//参数为自己设定的返回值
// 1. stat_routine,是被几个线程执行呢? 被10个线程执行,这个函数是可重入状态
// 2.该函数时可重入函数嘛?是可重入函数、
// 3.在函数内定义的变量,叫做局部变量,具有临时性--依旧适用--在多线程的情况下,没有问题
// 从实验结果来看,每个线程的cnt没有被共享,所以每个线程都有自己独立的栈结构
void *stat_routine(void* args)
{
    string str = static_cast<char*>(args);   //static_cast<>  安全的强制类型转换
    Thread_Data* td = static_cast<Thread_Data*>(args);
    int cnt=10;
    while(cnt--)
    {
        cout << "cnt: " << cnt << " &cnt: " << &cnt << endl; // bug       
        sleep(1);

        // int* p=nullptr;
        // p=nullptr;  //不报错
        // *p = 0;  //报错,在修改nullptr处的地址,此时整个进程会退出,只有线程出错,整个进程都会退出
        // exit(0);     //这不是线程退出,这是整个进程退出,任何一个执行流调用这个exit都会中止进程的
    }
    //线程退出方式:可以使用return   可以使用pthread_exit()
    //pthread_exit((void*)td->num);   //线程退出的函数  pthread_exit(void *retval);
    //

    //return (void*)td->num; // warning void* ret = (void*)td->number

    // //也可以单独封装一个类去接收这个值
    // ThreadReturn* tr =new ThreadReturn();
    // tr->exit_code=1;
    // tr->exit_result=111;
    // return (void*)tr;  //同样可以返回,这里是个右值,是需要拷贝的

    //return nullptr;     //这里是有返回值的,返回值以后线程结束

    return (void*)100;  //右值
}   

 线程退出的方式有两种方式:1、在线程函数结束的时候,直接return (void*)型参数;2、在线程结束的地方使用pthread_exit()函数,也可以返回一个退出码。

3、线程的等待

int pthread_join(pthread_t thread, void **retval);  //线程等待函数

//第一个参数:create函数的第一个参数
//第二个参数:输出型参数,是一个void**类型的

  //主线程一般是回收子线程的
    for(auto &xpid:td)
    {
        void* ret=0;        //注意这里是void* 类型的数据

         // 为什么没有见到,线程退出的时候,对应的退出信号??? 线程出异常,收到信号,整个进程都会退出!
         // pthread_join:默认就认为函数会调用成功!不考虑异常问题,异常问题是你进程该考虑的问题!
       int n = pthread_join(xpid->id,(void**)&ret);  
 //void ** retp ; *retp = return (void*)td->number   第二个参数是个输出型参数
        cout<<"回收子线程,namebuffer="<<xpid->namebuffer<<" 线程退出码xipd->num  "<<(long long*)ret<<endl;
        delete xpid;
        xpid=nullptr;
    }

 等待的输出型参数的位置,返回到线程退出处,使用的return返回的值或者pthread_exit返回的值。

4、线程的取消

int pthread_cancel(pthread_t thread);//取消线程,不再执行

for(int i=0;i<td.size()/2;++i)
{
  pthread_cancel(td[i]->id);  //取消线程的运行  int pthread_cancel(pthread_t thread);
  cout<<"pthread_cancel : "<<td[i]->namebuffer<<"success"<<endl;
}

线程同样是可以被取消的,取消后的退出码 是-1;线程被取消的前提是:这个线程已经跑起来的。

完整代码

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <pthread.h>
#include <string.h>
#include <vector>
#include <unistd.h>
using namespace std;
#define Num 10
class Thread_Data{
public:
    pthread_t id;
    char namebuffer[64];
    long long num;  //线程退出码,可以定义在这里面
};

class ThreadReturn{
public:
    int exit_code;
    int exit_result;

};

void *stat_routine(void* args)
{
    string str = static_cast<char*>(args);   //static_cast<>  安全的强制类型转换
    Thread_Data* td = static_cast<Thread_Data*>(args);
    int cnt=10;
    while(cnt--)
    {
        cout << "cnt: " << cnt << " &cnt: " << &cnt << endl; // bug       
        sleep(1);

    }

    return (void*)100;  //右值
}

int main()
{

vector<Thread_Data*>  td;
    //创建10个子线程
    for(int i=0;i<Num;++i)
    {
        Thread_Data* pid = new Thread_Data();   
        snprintf(pid->namebuffer,sizeof(pid->namebuffer),"%s:%d","threadnum",i+1);
        pid->num=i+1;
        pthread_create(&pid->id,nullptr,stat_routine,(void*)pid);    
        td.push_back(pid);
    }
    sleep(5);
    for(auto &xpid:td)
    {
        cout<<"name:"<<xpid->namebuffer<<"  id:"<<xpid->id<<endl;
    }

    for(int i=0;i<td.size()/2;++i)
    {
        pthread_cancel(td[i]->id);  //取消线程的运行  int pthread_cancel(pthread_t thread);
        cout<<"pthread_cancel : "<<td[i]->namebuffer<<"success"<<endl;
    }


    //主函数一般是回收子线程的
    for(auto &xpid:td)
    {
        void* ret=0;        
        int n = pthread_join(xpid->id,(void**)&ret);  
        cout<<"回收子线程,namebuffer="<<xpid->namebuffer<<" 线程退出码xipd->num  "<<(long long*)ret<<endl;
        delete xpid;
        xpid=nullptr;
    }

    sleep(5);
    return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值