Linux 学习笔记(三十一):线程概念、底层分页机制与初体验

一、 什么是线程?

在 Linux 中,线程是"轻量级进程(LWP)",是 CPU 调度的最小单位。一个进程可以包含多个线程。

  • 共享的资源:代码段、数据段(全局变量)、堆区、文件描述符表。

  • 私有的资源 :线程 ID、寄存器上下文、独立的栈(Stack)、errno、信号屏蔽字。

    线程切换极快(不需要切换页表和刷新 TLB),但也极其危险(一个线程崩溃会导致整个进程崩溃)。

二、 底层存储:为什么需要虚拟内存与多级页表?

操作系统通过"页表"将虚拟地址映射到物理地址。

  • 单级页表在 32 位系统下需要占用 4MB 且必须连续,极度浪费。

  • 因此引入了多级页表(页目录)。利用局部性原理,只加载需要的页表项。

  • 缺页中断(Page Fault) :当 CPU 访问的虚拟地址在页表中不存在时触发。OS 会挂起当前进程,去磁盘把数据读入物理内存,建立映射,然后恢复进程运行。这正是 malloc 申请大内存时,"延迟分配"的底层原理。

三、 实战:创建第一个线程

cpp 复制代码
#include <pthread.h>
pthread_create(&tid, NULL, thread_run, NULL);

注意: 编译时必须加上 -lpthread 链接库。

四、 实战验证与 LWP

使用 ps -aL 查看系统进程和线程信息,可以看到:

  • 两个执行流有相同的 PID,但 LWP 不同。Linux 内核并不区分进程和线程,它们底层都是 task_struct,只是共享资源的程度不同。

五、 实战:验证全局变量共享

新建子线程修改全局变量 g_val,主线程随后读取。发现主线程读到了修改后的值,完美验证了线程间数据(数据段)是共享的。

自我实战操作

先不用 C++ 的高大上封装,直接用 C 语言 POSIX 线程库。

bash 复制代码
cd ~/linux_study
mkdir thread_test
cd thread_test
实战 1:创造一个线程(并观察 LWP)
bash 复制代码
vim thread_create.c

(输入以下代码)

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void *thread_run(void *arg) {
    while(1) {
        printf("我是新线程,我的 ID 是: %lu\n", pthread_self());
        sleep(1);
    }
    return NULL;
}

int main() {
    pthread_t tid;
    // 创建线程,执行 thread_run 函数
    pthread_create(&tid, NULL, thread_run, NULL);
    
    while(1) {
        printf("我是主线程,我的 ID 是: %lu\n", pthread_self());
        sleep(1);
    }
    return 0;
}

编译并运行(注意坑点):

bash 复制代码
# 必须加 -lpthread,否则会报错 undefined reference to `pthread_create'
gcc thread_create.c -o thread_create -lpthread
./thread_create

👉 现象 :屏幕上会交错打印"主线程"和"新线程"的内容。

👉 神级操作 :新开一个终端,输入:

bash 复制代码
ps -aL | grep thread_create | head -n 1 && ps -aL | grep thread_create

你会看到什么?

  1. 有两个名为 thread_create 的进程。

  2. 它们的 PID 相同(因为是同一个进程)。

  3. 它们的 LWP(轻量级进程 ID,也就是线程 ID)不同!

    (这就证明了:Linux 内核并不区分进程和线程,统一用 task_struct 表示,只是共享了某些资源的 task_struct 就叫做线程。)

实战 2:验证"共享与私有"
bash 复制代码
vim thread_share.c

(输入以下代码)

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

int g_val = 100; // 全局变量(共享)

void *thread_run(void *arg) {
    g_val = 200; // 子线程修改全局变量
    printf("子线程:修改全局变量 g_val = %d\n", g_val);
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_run, NULL);
    
    sleep(1); // 等子线程先跑完
    printf("主线程:看到全局变量 g_val = %d\n", g_val); // 主线程读全局变量
    return 0;
}
bash 复制代码
 gcc thread_share.c -o thread_share -lpthread && ./thread_share

👉 现象 :主线程打印出来的是 200。这证明了全局变量是共享的 。如果是 fork 创建的子进程,这里打印的绝对还是 100(写时拷贝)。这就是线程间通信极其方便、但也极其危险的原因。

相关推荐
乌暮20 分钟前
Spring AOP 与 Spring 事务:用大白话彻底理解原理与实践
java·数据库·spring boot·后端·sql·学习·spring
分布式存储与RustFS1 小时前
Terraform 状态文件老在本地丢、并发还撞锁:把 state 锁进 RustFS
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
csdn_aspnet1 小时前
macOS 10 、Docker 玩轉 MySQL
运维·mysql·macos·docker·容器·mac
爱和冰阔落3 小时前
【Linux网络】TCP 半包粘包怎么解决?从长度报头、JSON 拆包到网络计算器完整实战
linux·运维·网络·tcp/ip
Go Work4 小时前
docker 占满根分区时先分清谁在吃盘
经验分享·笔记
3095566665 小时前
股票接口返回 404 与空数组的排查笔记:证书状态与级别参数
java·笔记·python·ios·iphone·股票数据api·股票数据接口
蒸蒸yyyyzwd5 小时前
从邓明学习的后端笔记
笔记·学习
say_fall5 小时前
【Linux网络编程】UDP套接字编程:系统编程组件、基础回显服务、InetAddr与地址转换、翻译服务与线程池聊天室
linux·服务器·网络·udp·vr
岑梓铭5 小时前
《考研408数据结构》第八章(排序)复习笔记
数据结构·笔记
机器学习之心5 小时前
基于变量相关图与残差学习的 GCN 多变量回归方法——混凝土 28 天抗压强度预测
学习·回归·gcn