线程眼中的线性地址空间

以前写过一篇《进程眼中的线性地址空间》,这是她的姊妹篇线程篇。而且和以前一样我们只谈32位Linux下的实现。另外读者可能还需要之前的一篇文章《Linux线程的前世今生》作为前期的辅助资料。

如果读者已经看过这两篇文章,那么我们就可以继续往下说了。

我简单列出上述文章中的几个要点:

  1. 32位操作系统下的每个进程拥有4GB的线性地址空间。
  2. 从Linux内核的角度来说,它并没有线程这个概念。在内核中,线程看起来就像是一个普通的进程(只是线程和其他一些进程共享某些资源,比如地址空间)。

暂时有这两点就可以了。我们直接就能从第二点中看出来,一个进程创建的所有线程实际上是都是在它的线性地址空间里运行的。也就是说,一个进程所创建的所有线程没有创建新的地址空间,而是共享着进程所拥有的4G的线性空间罢了。除了地址空间还共享什么呢?大致还有文件系统资源、文件描述符、信号处理程序以及被阻断的信号等内容。不过即便是共享地址空间,但是每个线程还是有自己的私有数据的,比如线程的运行时栈。

线程真的是共享这4G的地址空间吗?口说无凭,咱们来给出实证。我们给出验证代码1:

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

void *thread_func(void *args)
{
    printf("tid: %u pid: %u thread id: %un", getpid(), syscall(224), pthread_self());

    while(1) {
        sleep(10);
    }
}

int main(int argc, char *argv[])
{
    pthread_t thread;
    int count = 0;

    while (pthread_create(&thread, NULL, thread_func, NULL) == 0) {
        sleep(1);
        count++;
    }

    perror("Create Error:");
    printf("Max Count:%dn", count);

    return EXIT_SUCCESS;
}
 

从代码中我们能看出主线程每休眠一秒就创建一个新的线程,子线程始终睡眠不会退出。

我们在其创建了10来个线程后在终端按下Ctrl+Z键将其放到后台休眠,然后进入/proc目录下用这个进程PID命令的目录,查看maps文件。

这里只是部分输出,我们看到,子线程创建的所有的私有栈(stack:后面的即是线程在内核中拥有的实际PID值)就在其所属进程所拥有的这4G的线性地址空间里。

也许你已经猜到,倘若我们注释掉代码中主函数的sleep()函数,这个程序终将输出32位Linux在默认情况下一个进程所能创建出的线程的总数。注意不要注释掉线程中的sleep()函数,因为我们需要子线程一直存在而且不要占用太多的CPU资源。我们修改代码然后编译执行,结果如下:

我们看到,最后因为内存资源不足无法再创建线程了,总数是381(不过在我的机器上偶尔也会是380),再加上主线程就是382个。我们在《进程眼中的线性地址空间》中就知道一个线程默认的栈大小是8MB,8MB*382就是3056MB,因为其它诸如代码和全局数据也会占据一些空间,抛开内核占据的1GB,所以这些差不多就是用户空间所有的内存了。

P.S. 如果你要问,线程的私有栈在进程的地址空间里在何处分配?如何分配?我的答案是,请自行研究……maps里指明了地址范围的数值,结合进程的地址空间可以分析出来。另外在《Linux线程的前世今生》这篇文章的最后,我给出了NPTL库的两位作者写的文档,你可以参考阅读其中的章节。

上文中我们提到32位Linux默认线程创建的数量是382左右,那么我们想尝试创建更多的线程怎么办呢?修改默认栈大小就可以,我们既可以在代码中设置线程创建时的属性来设置,也可以在终端下使用ulimit命令来设置。

好了,我们继续。既然所有的线程在一个地址空间里,那….A线程在栈里创建的变量能否被B线程修改呢?答案是能,我们看代码:

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

int *p_num;

void *thread_1(void *args)
{
    int test_num = 1;

    printf("test_num: %dn", test_num);

    p_num = &test_num;

    sleep(2);

    printf("test_num: %dn", test_num);
}

void *thread_2(void *args)
{
    sleep(1);

    if (p_num != NULL) {
        *p_num = 2;
    }
}

int main(int argc, char *argv[])
{
    pthread_t thread1, thread2;

    pthread_create(&thread1, NULL, thread_1, NULL);
    pthread_create(&thread2, NULL, thread_2, NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    return EXIT_SUCCESS;
}
 

简单起见我没有使用什么条件变量之类的同步手段而是简单的采用sleep()函数来演示,大家明白就好。

编译运行,结果如我们所料。

其实站在共享的角度看,这篇到这里就差不多了,因为在《进程眼中的线性地址空间》中,其他的东西已经有了。虽然我觉得还是没多少干货,但确实也不知道再说些什么了。姑且先发布,以后有补充的再说。

时间: 2024-11-05 06:02:35

线程眼中的线性地址空间的相关文章

进程眼中的线性地址空间

从文章的题目我们就知道今天是以一个进程的角度来看待自身的运行环境.我们先提出第一个问题,什么是进程?对于这个问题,各种参考资料上给出的定义都显得过于抽象而难以理解,下面是我自己的定义: 进程是一个动态的概念,它是静态的可执行文件执行过程的描述,其包含了一个静态程序运行时的状态和其所占据的系统资源的总和. 还是很抽象吗?那么,我们可以这样比喻,如果说菜谱是程序代码,厨具是硬件的话,那么炒菜的整个过程就是一个进程.这下理解了吧?那我们继续. 每个程序在启动之后都会拥有自己的虚拟地址空间(Virtua

聊聊内存管理

这篇文章我们聊聊内存管理. 本来我想不针对于任何具体的操作系统来谈内存管理,但是又觉得不接地气.言之无物.所以我决定在阐述概念的同时,还针对IA32平台Linux下的内存管理做简要的介绍,并且以实验来证明结论.以下内容分拆为几个大标题和小节,内容前后承接. 物理地址空间 首先,什么是物理地址空间?我们知道CPU与外部进行信息传递的公用通道就是总线,一般而言,CPU有三大总线:控制总线.数据总线.地址总线.这三类总线在一定程度上决定了CPU对外部设备的控制和数据传送能力.其中地址总线决定了CPU能

Linux内核剖析 之 进程地址空间(三)

本节主要讲述缺页异常处理程序和堆的管理等内容. 缺页异常处理程序 触发缺页异常程序的两种情况: 1. 由编程错误引起的异常(如访问越界,地址不属于进程地址空间). 2. 地址属于线性地址空间,但内核还未分配相应的物理页,导致缺页异常. 缺页异常处理程序总体方案: 线性区描述符可以让缺页异常处理程序非常有效的完成它的工作. do_page_fault()函数是80x86上的缺页中断服务程序,它把引起缺页的线性地址和当前进程的线性区相比较,从而根据具体方案选择适当的方法处理此异常. 标识符vmall

Linux内核剖析 之 进程地址空间(一)

绪论     内核获取内存方式--直接了当:     1. 从分区页框分配器获取内存(__get_free_pages()或alloc_pages()):     2. 使用slab分配器为专用或通用对象分配内存(kmem_cache_alloc()或kmalloc()):     3. 使用vmalloc或vmalloc_32获取一块非连续内存区.     如果申请的内存得以满足,这些函数返回一个页描述符地址或线性地址.     *内核申请内存使用这些简单方法基于以下两个原因:     1.内

深入理解计算机系统-之-内存寻址(一)--存储管理机制(虚拟地址,线性地址,物理地址)

物理地址(physical address) 用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应. 这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样. 所以,说它是"与地址总线相对应",是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存

Linux内核设计与实现笔记(二) 内存管理、进程地址空间

内存管理 1.页   物理页作为内存管理的基本单位.内存管理单元通常以页为单位进行处理. 通过结构体page来表示系统中的每个物理页. 2.区 由于页位于内存中特定的物理地址上,所以不能将其用于一些特定的任务,故内核把页划分为不同的区. 硬件在内存寻址方面的问题: 一些硬件只能通过内存地址来执行直接内存访问(DMA) 一些体系结构其内存的物理寻址范围大于虚拟寻址范围,故,内存不能永久地映射到内核空间 解决方法,通过创建三种不同的分区: ZONE_DMA--专门执行DMA ZONE_NORMAL-

PHP及Zend Engine的线程安全模型分析_php技巧

不知道怎么回事总是令人不舒服的,因此我通过阅读源码和查阅有限的资料简要了解一下相关机制,本文是我对研究内容的总结. 本文首先解释了线程安全的概念及PHP中线程安全的背景,然后详细研究了PHP的线程安全机制ZTS(Zend Thread Safety)及具体的实现TSRM,研究内容包括相关数据结构.实现细节及运行机制,最后研究了Zend对于单线程和多线程环境的选择性编译问题. 线程安全 线程安全问题,一言以蔽之就是多线程环境下如何安全存取公共资源.我们知道,每个线程只拥有一个私有栈,共享所属进程的

linux内核分析之进程地址空间【转】

转自:http://blog.csdn.net/bullbat/article/details/7106094 版权声明:本文为博主原创文章,未经博主允许不得转载. 本文主要介绍linux内核中进程地址空间的数据结构描述,包括mm_struct/vm_area_struct.进程线性地址区间的分配流程,并对相应的源代码做了注释.  内核中的函数以相当直接了当的方式获得动态内存.当给用户态进程分配内存时,情况完全不同了.进程对动态内存的请求被认为是不紧迫的,一般来说,内核总是尽量推迟给用户态进程分

linux内存管理---虚拟地址、逻辑地址、线性地址、物理地址的区别(一)

  分析linux内存管理机制,离不了上述几个概念,在介绍上述几个概念之前,先从<深入理解linux内核>这本书中摘抄几段关于上述名词的解释: 一.<深入理解linux内核>的解释 逻辑地址(Logical Address)         包含在机器语言指令中用来指定一个操作数或一条指令的地址(有点深奥).这种寻址方式在80x86著名的分段结构中表现得尤为具体,它促使windows程序员把程序分成若干段.每个逻辑地址都由一个段和偏移量组成,偏移量指明了从段开始的地方到实际地址之间