欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

从地址空间看进程和线程

程序员文章站 2022-06-12 09:36:55
...

一、什么是进程

进程最经典的定义就是一个执行中的程序的实例。系统中的每个进程都运行在某个进程的上下文中。上下文是由程序正确运行的所需的状态组成的。这个状态包括放在内存中的程序代码和数据,栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符。

进程的地址空间如下图所示,注意的是这是每个进程都有的独立、私有的空间(代码段总是从0x400000开始的),这是通过虚拟内存技术实现的。
从地址空间看进程和线程

其中.data是已初始化的全局和静态变量
.bss是未初始化的全局和静态变量(不占空间,只是一个占位符)
.text是已经编译的程序机器代码

有了上图,再结合一个简单的例子就能明白我们写的程序中的各个变量在进程地址空间所处的位置,以及堆区和栈区的区别。

int a = 0; //全局初始化区
char *p1; //全局未初始化区
main()
{    
      int b; //栈
      char s[] = "abc";// 栈 
      char *p2;// 栈 
      char *p3 = "123456"; //123456\0在常量区,p3在栈上。 
      static int c =0// 全局(静态)初始化区 
      p1 = (char *)malloc(10); 
      p2 = (char *)malloc(20); 
      //分配得来得10和20字节的区域就在堆区。 
      strcpy(p1, "123456");// 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。 
}

二、什么是线程

线程是运行在进程上下文的逻辑流。一个进程里边可以运行着多个线程,所以线程的粒度比进程小,线程由内核调度,也有自己的线程上下文,包括一个唯一的整数线程ID, 栈和栈指针,程序计数器,通用目的寄存器和条件码。要注意的是,所有运行在一个进程里的线程共享该进程的整个虚拟地址空间。(结合上节的图)

线程的内存模型:
每个线程独立的线程上下文:一个唯一的整数线程ID, 栈和栈指针,程序计数器,通用目的寄存器和条件码。
和其他线程共享的进程上下文的剩余部分:整个用户虚拟地址空间,那就是上图的只读代码段,读/写数据段,堆以及所有的共享库代码和数据区域,也共享所有打开文件的集合。

这里要注意的是线程的寄存器是不共享的,通常栈区是被相应线程独立访问的,但是还是可能出现一个线程去访问另一个线程中的栈区的情况。这是因为这个线程获得了指向另一个线程栈区的指针,那么它就可以读写这个栈的任何部分。
来看这个例子:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define N 2

//线程函数
void *thread(void * vargp);
char **ptr;

int main()
{
    int i;
    pthread_t tid;
    char *msg[N] = {"hello from foo",
                    "hello from bar"}; //主线程栈上定义的msg
    ptr = msg;
    for( i=0; i < N; i++)
     pthread_create(&tid, NULL, thread, (void *)i);//创建两个对等线程
    pthread_exit(NULL);
}

//线程函数
void *thread(void *vargp)
{
    int myid = (int)vargp;
    static int cnt = 0;
    printf("[%d] : %s (cnt=%d)\n", myid, ptr[myid], ++cnt);//对等线程通过ptr引用主线程的msg,成功
    return NULL;
}

运行结果如下:
从地址空间看进程和线程

先创建了一个全局变量 ptr。在主线程的栈上定义了msg, 然后创建了两个对等线程。对等线程可以通过全局变量ptr间接引用主线程栈上的内容。

这里还要注意的是cnt是用static修饰的,那它是存在读/写数据段,所以两个线程对它是共享访问,所以最后是cnt=2。这里还要说明的是,两个线程运行的顺序是不确定的,这取决于系统的调度算法。