Linux

关注公众号 jb51net

关闭
首页 > 网站技巧 > 服务器 > Linux > 从内核视角理解Linux线程

Linux系统的线程入门:基本概念、虚拟内存、Linux内核线程、线程应用

作者:草莓熊Lotso

本文解释了线程的基本概念、虚拟内存、Linux内核中的线程实现,以及线程的优缺点和应用场景,线程是进程内部的执行流,共享进程资源但拥有独立的执行上下文,线程的优点包括创建和切换快、资源占用少等缺点包括性能损失、健壮性降低和编程复杂

大家好,本篇是Linux系统编程系列的线程入门,我会结合底层原理,把线程是什么、为什么需要虚拟内存、Linux 线程本质、优缺点与用途一次性讲透,帮你从内核视角真正理解线程。

一. 什么是线程?一句话抓住本质

线程 = 进程内部的一条执行流 / 控制序列

一句话区分进程与线程:进程是资源分配的基本单位;线程是 CPU 调度的基本单位



📌 不过:

  • 仅仅有上面的理解,是不够的
  • 要真正理解线程,就必须搞清楚,内核是如何进行资源划分的,尤其是代码

二. 必须先懂:虚拟地址空间与分页机制

线程之所以能 “共享、轻量化”,完全依赖虚拟地址空间。这部分是理解线程的地基。

2.1 没有虚拟内存会怎样?

早期操作系统没有虚拟内存:

我们希望:

于是:虚拟地址空间 + 分页 + 页表 诞生。

2.2 分页基本概念

机制

2.3 物理内存管理(重点看图)

以 4GB 物理内存、4KB 页框为例:总页数 = 4GB / 4KB = 1048576 个页框
内核用 struct page 表示系统中的每个物理页。为节省内存,struct page 大量使用 union(联合体)。

struct page {
    /* 原子标志,有些情况下会异步更新 */
    unsigned long flags;

    union {
        struct {
            /* 换出页列表,例如由 zone->lru_lock 保护的 active_list */
            struct list_head lru;

            /* 如果最低位为 0,则指向 inode 的 address_space,或为 NULL;
             * 如果页映射为匿名内存,最低位置位,且该指针指向 anon_vma 对象
             */
            struct address_space *mapping;

            /* 在映射内的偏移量 */
            pgoff_t index;

            /*
             * 由映射私有,不透明数据
             * - 如果设置了 PagePrivate,通常用于 buffer_heads
             * - 如果设置了 PageSwapCache,则用于 swp_entry_t
             * - 如果设置了 PG_buddy,则用于表示伙伴系统中的阶
             */
            unsigned long private;
        };

        struct { /* slab, slob and slub */
            union {
                struct list_head slab_list; /* 复用 lru */
                struct { /* Partial pages */
                    struct page *next;
#ifdef CONFIG_64BIT
                    int pages;    /* 剩余页数 */
                    int pobjects; /* 近似对象计数 */
#else
                    short int pages;
                    short int pobjects;
#endif
                };
            };
            struct kmem_cache *slab_cache; /* 不用于 slob */
            /* 双字边界对齐 */
            void *freelist;                /* 第一个空闲对象 */
            union {
                void *s_mem;               /* slab: 第一个对象 */
                unsigned long counters;    /* SLUB: 计数器 */
                struct { /* SLUB 专用 */
                    unsigned inuse : 16;   /* 已使用的对象数 */
                    unsigned objects : 15; /* 总对象数 */
                    unsigned frozen : 1;   /* 是否冻结 */
                };
            };
        };
        /* 其他可能的联合成员(如用于文件系统等) */
        ...
    };

    union {
        /* 内存管理子系统中映射的页表项计数,用于表示页是否已经映射,
         * 还用于限制逆向映射搜索 */
        atomic_t _mapcount;
        unsigned int page_type;
        unsigned int active; /* SLAB */
        int units;           /* SLOB */
    };

    /* 其余字段(如引用计数、私有用例等) */
    ...

#if defined(WANT_PAGE_VIRTUAL)
    /* 内核虚拟地址(如果没有映射则为 NULL,即高端内存) */
    void *virtual;
#endif /* WANT_PAGE_VIRTUAL */

    /* 后续可能还有其他成员,取决于内核配置 */
    ...
};

关键成员

内存开销计算:struct page 约占 40 字节。4GB 内存共 1048576 个 page:总消耗 = 1048576 * 40B ≈ 40MB。相对于 4GB 内存可以忽略。

页大小的权衡


2.4 页表(单级页表)

页表中每一个表项,指向一个物理页的起始地址。
32 位系统 4GB 虚拟空间:总表项 = 4GB / 4KB = 1048576 项每项 4 字节 → 页表总大小 4MB。

问题:单级页表需要连续 1024 个物理页框存储。我们用分页解决物理连续,结果页表自己又要连续内存。
同时,根据局部性原理,进程只使用少量页,不需要全量页表。

2.5 多级页表(二级页表)&& 地址转换

解决思路:把页表再分页
结构

虚拟地址划分(32 位、4KB 页):10 位页目录 | 10 位页表 | 12 位页内偏移


地址转换流程


2.6 TLB 快表(Translation Lookaside Buffer)

多级页表虽然省内存,但访问变慢(多次访存)。
解决方案:MMU 集成TLB 缓存,流程如下:

  1. CPU 给出虚拟地址
  2. MMU 先查 TLB
  3. 命中:直接得到物理地址
  4. 不命中:查页表,并把映射写入 TLB

TLB 命中率极高,极大加速地址翻译。

2.8 缺页异常 Page Fault

当虚拟地址在 TLB 与页表中都找不到物理页时,触发缺页异常。这是硬件中断,可由软件修复。

缺页异常分为三类:

三. Linux 线程本质:LWP 轻量级进程

Linux 内核没有专门的线程结构体!

测试样例用到的代码

// #include <iostream>
// #include <thread>
// #include <unistd.h>

// // C++中的线程
// void hello()
// {
//     while(true)
//     {
//         std::cout << "我是新进程..., pid: " << getpid() << std::endl;
//         sleep(1);
//     }
// }


// int main()
// {
//     std::thread t(hello);

//     while(true)
//     {
//         std::cout << "我是主线程..., pid: " << getpid() << std::endl;
//         sleep(1);
//     }

//     t.join();
//     return 0;
// }

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

// Linux中封装的线程 -- 其实Linux是只有轻量级进程的概念的
void *hello(void *args)
{
    while(true)
    {
        const char *name = (const char*)args;
        std::cout << "我是新线程..., pid: " << getpid() << " name is : "<< name <<std::endl;
        sleep(1);
    }
}


int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, hello, (void*)"new-thread");

    while(true)
    {
        std::cout << "我是主线程..., pid: " << getpid() << std::endl;
        sleep(1);
    }

    return 0;
}

3.1 线程资源共享 && 线程私有资源

同一进程内所有线程共享:

每个线程独立拥有:

3.2 线程栈位置 && Linux中线程的理解图示

四. 线程的优缺点和异常与用途

4.1 线程的优点


4.2 线程的缺点

int g_val = 100;
int *p = nullptr;

void hello(const std::string &name) {
    printf("haha, I am common function!, %s\n", name.c_str());
    sleep(5);
}

void *threaddrun1(void *args)
{
    p = (int*)malloc(sizeof(int) * 10);
    std::string threadname = static_cast<const char*>(args);
    while(true)
    {
        printf("%s is running, g_val: %d, &g_val: %p\n", threadname.c_str(), g_val, &g_val);
        sleep(1);
        hello(threadname);
    }
}

void* threaddrun2(void *args)
{
    std::string threadname = static_cast<const char*>(args);
    while(true)
    {
        printf("%s is running, g_val: %d, &g_val: %p\n", threadname.c_str(), g_val, &g_val);
        sleep(1);
        g_val++;
        hello(threadname);
    }
}

int main()
{
    pthread_t t1, t2;
    pthread_create(&t1, nullptr, threaddrun1, (void*)"thread-1");
    pthread_create(&t2, nullptr, threaddrun2, (void*)"thread-2");
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    return 0;
}


4.3 线程的异常与用途(异常上面的缺点中也涉及到了)

异常

用途
• 合理的使用多线程,能提高CPU密集型程序的执行效率
• 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们⼀边写代码一边下载开发工具,就是多线程运行的⼀种表现)

4.4 最简单总结

结尾

结语:线程是 Linux 并发编程的核心基石,理解其内核本质、与进程的核心区别、优缺点和适用场景,是后续掌握线程控制、同步互斥、线程安全的关键。希望这篇博客能帮你吃透线程的基础概念,后续我也会继续分享线程控制、地址空间布局等进阶内容,欢迎点赞收藏,一起交流学习~

到此这篇关于Linux系统的线程入门:基本概念、虚拟内存、Linux内核线程、线程应用的文章就介绍到这了,更多相关从内核视角理解Linux线程内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文