task_struct是用来描述进程的,它里面有一个指针指向mm_struct(虚拟地址空间)

在地址空间中,栈区可以可以由ebp,esp来进行限定它的区域,那么堆区怎么来确定呢?

怎么知道每次开辟的空间是多大范围的呢?这里还有一个结构来描述每次开辟堆区的大小——vm_area_struct,该结构的start,end就可以确定堆区的大小,该结构为双向链表。

Linux线程_#include


下面我们来讲解一下虚拟地址到物理地址是怎么映射的。

不管是磁盘还是内存都是以4KB为单位的(其中管理内存的4kb的结构为struct page),并且可执行程序的文件格式为ELF格式的——即每个程序的各个区域都是确定的。拿32位的平台为例,如果让每个虚拟地址和物理地址进行映射,且只提供一个页表,那么只存储页表这个结构都会使内存不够用,那么我们要怎么办呢?

对于32为的虚拟地址,前10位作为键与1级页表映射,映射的结果与二级页表关联,中间10位作为键值与第2级页表映射,映射的结果加上后12位(后12位称为页内偏移)确定物理地址。

这里说明一下名词,磁盘中的4kb为页桢,内存中的4kb为页框

Linux线程_#include_02


什么叫做线程

我们创建一个进程,通过一定的技术手段,将当前进程的“资源”以一定的方式划分给不同的task_struct,对于该子进程来言,它没有创建内存地址空间,也没有在自己独立的页表,那么我们把这种task_struct叫做线程。

线程在进程内部执行,是OS调度的基本单位。这里的在进程内部执行是线程在进程的地址空间内运行,对于cpu来说,cpu不关心执行流是进程还是线程,它只关注pcb。

Linux中的进程统称为——轻量级线程。Linux中没有真正意义上的线程结构(没有线程的数据结构),它是用进程模拟线程的。
Linux并不能直接提供给我们相关的接口,只能提供轻量级进程的接口!在用户层实现了一套用户层多线程方案以库的方式提供给用户进行使用——pthread线程库,该库为原生的线程库,那么编译的时候就要注意链接该库。

那么什么叫进程呢?

从内核的视角:承担分配系统资源的实体——即进程向操作系统要资源,而线程向进程要资源 从用户的角度: 线程就是数据结构+代码+数据——这里的数据结构包括该程序所有的task_struct结构体

如何理解我们之前写的代码呢?
我们之前的写的代码,都是没有多进程的,我们叫做单执行流的进程。
task_struct就可以理解为一个执行流。

线程是如何看待进程内部资源的
一部分资源是共享的,一部分资源是线程私有的。
共享的比如有:全局数据,文件描述符表,信号等。
私有的有:线程ID,线程的上下文寄存器,栈,(加黑的字体体现了进程的动态属性)errno错误码,信号屏蔽字,调度优先级。

为什么线程切换的成本更低?
地址空间和页表不需要切换,还有就是,CPU内部有1~3级缓存,代码和数据会预读到CPU的缓存中,线程切换的时候,不需要重新缓存。而进程切换的时候需要重新进行缓存,就会导致成本的增加。


简单的使用:

创建一个新的线程——pthread_create

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

thread用来返回线程的id,它的类型为typedef unsigned long int pthread_t;attr使用默认的就可以。 start_routine是一个函数指针,arg为参数,传给start_routine的。

下面是简单的使用:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

using namespace std;

void *fun(void *name)
{
    string s=(char*)name;
    while (true)
    {
        cout << s<< "pid:" << getpid() << endl<<endl;
        sleep(1);
    }
}
int main()
{
    pthread_t id[5];
    char name[32];
    for(int i=1;i<=5;i++)
    {
        snprintf(name,sizeof name,"%s-%d","thread",i);
        pthread_create(id,nullptr,fun,(void*)name);
        sleep(1);
    }
    while(true)
    {
        cout<<"main,pid:"<<getpid()<<endl;
        sleep(3);
    }

    return 0;
}

ps -aL | head -1 && ps -aL | grep mythread可以查看线程

PID   LWP TTY          TIME CMD
 5713  5713 pts/4    00:00:00 mythread
 5713  5714 pts/4    00:00:00 mythread
 5713  5717 pts/4    00:00:00 mythread
 5713  5719 pts/4    00:00:00 mythread
 5713  5726 pts/4    00:00:00 mythread
 5713  5740 pts/4    00:00:00 mythread

LWP为线程的id

继续看几个接口:

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

线程等待,第一个参数为线程id,第二个参数为接受线程的返回值

#include <pthread.h>
void pthread_exit(void *retval);

该退出结果会返回给主线程

#include <pthread.h>
int pthread_cancel(pthread_t thread);

线程取消,参数为线程id

  1. 线程被取消,join的时候,退出码是-1 #define PTHREAD_CANCELED ((void *) -1)

线程一旦异常,导致整个进程退出

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

using namespace std;

void* threadRoutine(void * arg)
{
    while(true)
    {
        cout<<"thread runing:"<<(char*)arg<<endl;
        sleep(1);
        int a=100;
        a/=0;
    }
}

int main()
{
    pthread_t id;
    pthread_create(&id,nullptr,threadRoutine,(void *)"thread 1");

    while(true)
    {
        cout << "main runing......" << endl;
        sleep(2);
    }

    return 0;
}

结果:

[ml@VM-4-8-centos 线程]$ ./mythread 
main runing......
thread runing:thread 1
Floating point exception

线程在创建并执行的时候,线程也是需要进行等待的,如果主线程如果不等待,即会引起类似于进程的僵尸问题,导致内存泄漏

pthread_t id;这个本质上是一个地址

Linux线程_#include_03


为什么这个id和LWP不是一样的呢?

因为我们目前使用的不是Linux自带的创建线程的接口,用的是pthread库中的接口。

这个地址是库给我们返回的线程私有栈的地址,在创线程的时候,一部分是系统内核提供的轻量级线程的结构,还有一部分是内核无法实现的,放在pthread中实现的,比如线程的私有库。

Linux线程_#include_04


__thread修饰全面变量,带来的结果就是让每一个线程各自拥有一个全局变量——线程的局部存储。

如果一个线程进行程序替换,则整个进程进行替换。

分离线程

默认的情况下,新创建的线程在退出的时候,主线程需要对子线程进行pthread_join操作,否则无法释放资源,从而造成资源的泄漏。如果不关系线程的返回值,这时候我们可以不用进行等待,可以直接告诉系统,当线程退出的时候,自动释放资源。那么我们就可以对线程进行分离线程。该接口为:

#include <pthread.h>
int pthread_detach(pthread_t thread);


线程互斥

Linux线程_#include_05

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <cstdio>
using namespace std;

int ticket=10000;

void* threadRoutine(void * arg)
{
    string s((char*)arg);
    while(true)
    {
        if(ticket>0)
        {
            usleep(1000);
            ticket--;
            cout<<s<<"剩余:"<<ticket<<"张票"<<endl;
        }
        else{
            break;
        }
    }
}

#define NUMS 5

int main()
{
    pthread_t pt[NUMS];
    char name[64];
    for(int i=1;i<=NUMS;i++)
    {
        snprintf(name,sizeof name,"%s:%d","thread",i);
        pthread_create(pt+i-1,nullptr,threadRoutine,(void*)name);
        usleep(1);
    }

    for(int i=0;i<NUMS;i++)
    {
        pthread_join(pt[i],nullptr);
    }
    return 0;
}

对于上面的代码的,我们会发现它会显示负数余票。就是因为对于线程来说,票ticket是共享资源。对于共享资源,我们要进行加锁,如果不进行加锁,会因为并发共享的原因,导致错误的结果。

关于互斥量的一些参数:

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
      const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

互斥量初始化的两种形式:

  1. 静态分配——直接全局变量,进行初始化PTHREAD_MUTEX_INITIALIZER
  2. 动态分配——用该函数pthread_mutex_init,第一个参数为互斥量,第二个参数可以为空

销毁互斥量——pthread_mutex_destroy

  • 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

加锁解锁:

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
成功返回0,失败返回错误码。

对上面的资源进行加锁:

int ticket=10000;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

void* threadRoutine(void * arg)
{
    string s((char*)arg);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        if(ticket>0)
        {
            usleep(1000);
            ticket--;
            pthread_mutex_unlock(&mutex);
            // printf("%s,剩余%d张票,%lu\n",name,ticket,pthread_self());
            cout<<s<<"剩余:"<<ticket<<"张票"<<endl;
        }
        else{
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
}

另一种形式的初始化加锁方式:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <cstdio>
using namespace std;

int ticket=10000;
struct DataThread
{
    string _s;
    pthread_mutex_t* _mutex;
    DataThread(string& s,pthread_mutex_t* mutex)
    :_s(s),_mutex(mutex)
    {}
};

void* threadRoutine(void * arg)
{
    DataThread* data=(DataThread*)arg;
    while(true)
    {
        pthread_mutex_lock(data->_mutex);
        if(ticket>0)
        {
            usleep(1000);
            ticket--;
            pthread_mutex_unlock(data->_mutex);
            cout<<data->_s<<"剩余:"<<ticket<<"张票"<<endl;
        }
        else{
            pthread_mutex_unlock(data->_mutex);
            break;
        }
    }
    delete data;
    return nullptr;
}

#define NUMS 5
int main()
{
    pthread_t pt[NUMS];
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex,nullptr);
    for(int i=1;i<=NUMS;i++)
    {
        string s("thread");
        s+=i+'0';
        DataThread* data=new DataThread(s,&mutex);
        pthread_create(pt+i-1,nullptr,threadRoutine,(void*)data);
    }

    for(int i=0;i<NUMS;i++)
    {
        pthread_join(pt[i],nullptr);
    }
    pthread_mutex_destroy(&mutex);
    return 0;
}

创建线程的时候,传入的是动态开辟的空间。

mutex简单理解就是一个0/1的计数器,用于标记资源访问状态:

  • 0表示已经有执行流加锁成功,资源处于不可访问,
  • 1表示未加锁,资源可访问。

问题思考:

互斥量也是多线程都能看见的资源,那么它是怎么保证原子性的呢?

在加锁和解锁的时候,在汇编层的时候就已经保证了原子性。

Linux线程_互斥量_06


如上图所示,

Linux线程_互斥量_07

一条汇编指令本身就是原子的,该指令的作用就是把寄存器和内存单元的数据进行交换,使之任何一个线程在访问临界资源的时候,有且只有一个执行流执行(一个线程)。从而保证了访问临界资源的原子性。关于可重入和线程安全:

可重入的一定是线程安全的,但是线程安全的不一定是可重入的。

Linux线程_互斥量_08


Linux线程_加锁_09

死锁:

一个线程申请2把锁,另一个线程也申请2把锁,但是它们申请的顺序不一样。

线程1先申请锁1,后申请锁2;线程2先申请锁2,后申请锁1。

这就会导致死锁。当然这只是一个例子。

造成死锁要具备4个必要条件。

Linux线程_加锁_10


如何避免死锁呢?

只需要破坏上面四个必要条件中的其中一个即可。

Linux线程同步

什么叫做Linux线程同步,就是让线程能够按照某种特定的顺序访问临界资源,从而避免因为竞争资源导致的饥饿问题,就是同步。
在我们之前写的代码中,我们会发现创建的多个线程中,只有一个线程在执行抢票的代码。
那么对于其他线程来说,因抢不到资源导致处于饥饿问题。且对于这些线程,它们会一直去临界区进行访问,看是否满足条件。
那么我们应该怎么处理这两个问题。


方案1:条件变量

当我们申请临界资源的前,要先做临界资源是否存在的检查的,要做检查的本质也是访问临界资源。所以我们得出的结论是:对临界资源的检查也应该放在加锁和解锁之间。

对于上面2个问题,我们可以这样解决:

  1. 不要让线程频繁自己检测——等待
  2. 当条件就绪的时候,通知对于的线程,让他进行资源的申请和访问。

下面介绍一下接口:
初始化

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,
      const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

通过上面的接口我们发现条件变量和互斥量差不多
初始化的形式也是有两种情况。
等待:

#include <pthread.h>

int pthread_cond_timedwait(pthread_cond_t *restrict cond,
      pthread_mutex_t *restrict mutex,
      const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,
      pthread_mutex_t *restrict mutex);

第一个接口为在设置特定的时间内唤醒线程。
第二个接口中,第一个参数cond为条件变量,第二个参数mutex为互斥量
唤醒等待:

#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

int pthread_cond_signal(pthread_cond_t *cond);传入条件变量参数进行唤醒线程。
pthread_cond_broadcast把等待的线程全部唤醒。


简单的使用一下

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

using namespace std;

#define NUMS 5
volatile bool quit=false;
typedef void(*funs)(const string&,pthread_mutex_t* ,pthread_cond_t*);

struct Mycond
{
    string _name;
    funs _fun;
    pthread_mutex_t* _mutex;
    pthread_cond_t* _cond;
    Mycond(string& name,funs fun, pthread_mutex_t* mutex,pthread_cond_t* cond)
    :_name(name),_fun(fun),_mutex(mutex),_cond(cond)
    {}
};

void *func(void *args)
{
    Mycond *mycond = (Mycond *)args;
    // 处理
    mycond->_fun(mycond->_name,mycond->_mutex,mycond->_cond);

    delete mycond;
    return nullptr;
}
void func1(const string &name, pthread_mutex_t *mutex, pthread_cond_t *cond)
{
    while (!quit)
    {
        pthread_mutex_lock(mutex);
        pthread_cond_wait(cond, mutex);
        cout << name<<"起床" << endl;

        pthread_mutex_unlock(mutex);
    }
}
void func2(const string &name, pthread_mutex_t *mutex, pthread_cond_t *cond)
{
    while (!quit)
    {
        pthread_mutex_lock(mutex);
        pthread_cond_wait(cond, mutex);
         cout << name << "吃饭" << endl;

        pthread_mutex_unlock(mutex);
    }
}
void func3(const string &name, pthread_mutex_t *mutex, pthread_cond_t *cond)
{
    while (!quit)
    {
        pthread_mutex_lock(mutex);
        pthread_cond_wait(cond, mutex);
        cout << name << "学习" << endl;

        pthread_mutex_unlock(mutex);
    }
}
void func4(const string &name, pthread_mutex_t *mutex, pthread_cond_t *cond)
{
    while (!quit)
    {
        pthread_mutex_lock(mutex);
        pthread_cond_wait(cond, mutex);
        cout << name << "运动" << endl;

        pthread_mutex_unlock(mutex);
    }
}
void func5(const string &name, pthread_mutex_t *mutex, pthread_cond_t *cond)
{
    while (!quit)
    {
        pthread_mutex_lock(mutex);
        pthread_cond_wait(cond, mutex);
        cout << name << "睡觉" << endl;

        pthread_mutex_unlock(mutex);
    }
}

int main()
{
    pthread_t tids[NUMS];
    funs fun[NUMS]={func1,func2,func3,func4,func5};
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    pthread_mutex_init(&mutex,nullptr);
    pthread_cond_init(&cond,nullptr);

    for(int i=0;i<NUMS;i++)
    {
        string s("thread");
        s+=to_string(i+1);
        Mycond* myconddate=new Mycond(s,fun[i],&mutex,&cond);
        pthread_create(tids+i,nullptr,func,(void*)myconddate);
    }
    int count=5;
    while(count)
    {
        //唤醒
        cout<<"唤醒线程"<<count--<<endl;
        pthread_cond_signal(&cond);
        // pthread_cond_broadcast(&cond);
        sleep(1);
    }
    quit=true;
    //唤醒全部,让之退出
    pthread_cond_broadcast(&cond);
    for(int i=0;i<NUMS;i++)
    {
        pthread_join(tids[i],nullptr);
    }
    cout<<"销毁"<<endl;
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    return 0;
}


生产者消费者模型

在生产者消费者模型中,有两种角色——生产者、消费者;一个交易场所——超市
它们之间有几种关系:

  1. 生产者和生产者:竞争、互斥
  2. 消费者和消费者:竞争、互斥
  3. 生产者和消费者:互斥、同步

对于生产者、消费者用线程进行模拟。而超市可以当成为缓冲区(可以为某种数据结构)。

生产者生产的数据是从哪里来的——比如网络上传输的
消费者如何使用数据——对数据进行相应的处理


基于阻塞队列的模型

当被阻塞的时候,为什么要传入锁呢?因为要把锁给释放掉,让其他进行继续可以获得锁。
当被唤醒的时候,从当前位置进行唤醒,然后从阻塞的位置往下继续运行,那么会自动获取到锁,保证在持有锁的条件下执行代码的。
代码演示:
https://gitee.com/maoleblog/linux/tree/master/%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%E6%A8%A1%E5%9E%8B


信号量

互斥量那里,在临界区域里面加锁,是对整体的资源进行加锁,只允许一个执行流使用该资源。如果共享资源非常多,且一个执行流不会全部的去使用它,而是让不同的执行流去执行该共享区域内的不同资源;只有当不同的执行相同的区域时,才进行加锁。
那么对于上面的问题,我们要思考一下,

  1. 怎么才能知道要用多少个资源可以被其他执行流去执行呢?还剩多少个资源呢?
  2. 怎么才能保证分给我的资源是我想要的那个资源呢?

从上面的问题中,我们就可以引出信号量。
上面的多少个资源就用信号量去表示——无论是还剩多少资源还是有多少个资源。

这里的信号量只是表示它有获得了访问资源的资格,访问不访问是另外一件事,但是肯定会有它访问的资源。
信号量的本质是一个计数器,访问临界资源的时候,必须先申请信号量——也就是P操作,使信号量减少一个;
使用完资源就要释放信号量——也就是V操作,使信号量加1。
如何理解信号量的使用:

我们申请一个信号量,当前执行流一定具有一个资源,可以被它使用啦,是哪个资源呢?需要程序员结合场景由程序员编码完成。


初始化信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

第一个参数:信号量指针;第二个参数:0表示线程间共享信号量、非零表示进程间共享
第三个参数:信号量初始值

销毁信号量

#include <semaphore.h>
int sem_destroy(sem_t *sem);


等待信号量

也就是P操作

#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);


发布信号量

也就是V操作

#include <semaphore.h>
int sem_post(sem_t *sem);


基于环形队列的生产消费模型

如果只有一个消费者和一个生产者,那么对于环形队列来说,只有环形队列为满、为空的时候
才会导致放入和去除的对同一区域进行访问。除此之外,两者都就可以并发的访问。

放入——生产者;去除——消费者。
如果生产者和消费者指向环形结构的同一个位置(为空,满的时候):生产者和消费者就会出现互斥或者同步问题。

为空的时候:生产者必须先访问资源,而消费者必须进行等待生产者生产出来才能进行消费为满的时候:生产者必须等待,让消费者先进行消费。生产者不能覆盖消费者消费者不能超过生产者

下面是生产者消费者的PV操作:
spaceSem->可用空间的信号量->初始为N(队列的总量)
dataSem->已有数据的信号量->初始为0

下面这个为单消费者生产者,如果有多个消费者生产者的话,需要加锁。
因为多个的话,会出现生产者和生产者之间的互斥竞争关系,以及消费者和消费者的互斥竞争关系。
生产者:
P(spaceSem)
生产资源
V(dataSem)
消费者:
P(dataSem)
消费资源
V(spaceSem)

代码演示:
https://gitee.com/maoleblog/linux/tree/master/%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%E6%A8%A1%E5%9E%8B

线程池

线程池中维护着多个线程,等待着监督管理者分配并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用内核,还能防止过分调度。

Linux线程_互斥量_11


用饿汉模式实现单例模式(线程安全)

单例模式,一个类只能有一个对象

在多线程同时访问下面这个函数的时候,这个就是共享资源,那么我们就要对临界区进行加锁操作。

Linux线程_加锁_12

Linux线程_#include_13


自旋锁:

当线程进入临界资源的时候,其他进程就要被挂起等待,但是如果线程执行临界区代码的时候用的时间非常少的时候,那么我们就不需要把其他线程挂起等待;让其他线程进行循环式的检测临界区的资源是否就绪就可以。实现这种临界资源的访问功能就是自旋锁。读者写者问题,可以看一下课件。

Linux线程_互斥量_14

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux