Linux——进程、线程

Q&A

  • 运行一个c程序,相当于哪个系统调用 – 用execve()系统调用。

  • 进程和线程?

    • 进程是资源分配的最小单位,进程有独立地址空间,多个线程是共享地址空间和公共变量
    • 线程是cpu时间片分配的最小单位,程序执行的最小单位。
    • 一个进程至少包含一个线程,即主线程
  • ps 查看所有子进程?pstree -p pid

  • ps -eLf各字段含义?

    PID:process id 进程id
    PPID: parent process id 父进程id
    LWP:表示这是个线程;要么是主线程(进程),要么是线程
    NLWP: num of light weight process 轻量级进程数量,即线程数量
    TIME: 占用的CPU总时间
    C CPU利用率,以整数表示。
    
  • 查看进程下的所有线程cpu利用率/内存/优先级等信息?top -H -p 25120

  • 查看线程在哪个CPU上运行?ps -eo ruser,pid,ppid,lwp,psr,args -L |grep l3-agent

  • 常见的进程状态?

    -D:不可被唤醒的睡眠状态,通常用于 I/O 情况。
    -R:该进程正在运行。
    -S:该进程处于睡眠状态,可被唤醒。
    -T:停止状态,可能是在后台暂停或进程处于除错状态。
    -W:内存交互状态(从 2.6 内核开始无效)。
    -X:死掉的进程(应该不会出现)。
    -Z:僵尸进程。进程已经中止,但是部分程序还在内存当中。
    -<:高优先级(以下状态在 BSD 格式中出现)。
    -N:低优先级。
    -L:被锁入内存。
    -s:包含子进程。
    -l:多线程(小写 L)。
    -+:位于后台。
    
  • 进程的六种状态及其转换过程(生命周期)——就绪态,运行态,深度睡眠态,浅睡眠态,停止态,僵尸态。

  • 父子进程共享的是?不同的是?——fork后,父子进程共享有(文件描述符、mmap建立的映射区。其他都是复制了一份,只是该进程的进程空间甚至进程地址完全跟父进程看着一样,但是完全是独立的空间了)

  • fork与vfork的区别——vfork用于创建一个新进程,而该新进程的目的是exec一个新程序。区别一:不将父进程的地址空间复制到子进程。区别二:vfork保证子进程先运行,在它调用exec或(exit)之后父进程才可能被调度运行。

  • **共享内存的地址值是在用户进程空间范围之内么?**fork之后子进程使用该地址和父进程是一样的么?如果是不同的进程,共享内存的进程空间中显示的地址是一样的么?

  • 进程的生命周期,就绪态和运行态在数值上是相等的,都是由宏TASK_RUNNING定义。

  • Linux中Task_struct是如何被管理的呢?

  • 理解僵尸进程——放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置(资源已经释放,Task_struct结构还在)。如果他的父进程没安装SIGCHLD信号处理函数调用wait()/waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。如果父进程不会结束,子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。系统所能使用的进程号是有限的(cat /proc/sys/kernel/pid_max),如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。

  • 子进程回收:

    1. 父进程通过wait/waitpid等函数等待子进程结束,这会导致父进程挂起。
    2. 如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后,父进程会收到该信号,可以在handler中调用wait回收。
    3. 如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN) 通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号
    4. 还有一些技巧,就是fork两次,父进程fork一个子进程,然后继续工作,子进程fork一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要自己做。
  • 子进程结束后为什么要进入僵尸状态?——因为父进程可能要取得子进程的退出状态等信息。

  • 僵尸状态是每个子进程必经的状态吗?——任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构(它占用一点内存 资源,也就是进程表里还有一个记录),等待父进程处理。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。

  • 僵尸进程清除的方法:

    1. 改写父进程,在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信 号后,执行waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用wait,内核也会向它发送SIGCHLD消息,尽管对的默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。
    2. SIGCHLD信号:子进程结束时, 父进程会收到这个信号。如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程来接管)。
  • 进程终止时exit()函数,那么线程终止是什么呢?——线程终止的三种情况:

    1. 线程只是从启动函数中返回,返回值是线程的退出码。
    2. 线程可以被同一进程中的其他线程取消。
    3. 线程调用 pthread_exit
  • 如果不等待一个线程,同时对线程的返回值不感兴趣,可以设置这个线程为被分离状态,让系统在线程退出的时候自动回收它所占用的资源。一个线程不能自己调用pthread_detach改变自己为被分离状态,只能由其他线程调用pthread_detach

  • pthread_cancel() 允许一个线程取消 th 指定的另一个线程。

  • 进程——资源分配的最小单位,线程——程序执行的最小单位。进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间),一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

  • 使用多线程的理由?

    • 和进程相比,它是一种非常"节俭"的多任务操作方式。在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。
    • 线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

进程

进程是资源分配的基本单位,线程是调度的基本单位

进程信息

Linux调度器实际是识别task_struct进行调度。无论进程线程,底层都对应一个task_struct,进程和线程的区别是共享资源的多少,两个进程间完全不共享资源,两个线程间共享所有资源。

PCB (Process Control Block) 进程控制块

task_struct 就是 Linux 内核对于一个进程的描述,也可以称为「进程描述符」。存放着进程所需要的所有资源的结构的描述。/proc/${pid} 进程相关信息。对于操作系统,进程就是一个数据结构。

struct task_struct {
    long              state; // 进程状态  -1为不可运行, 0为可运行, >0为已中断
    
    struct mm_struct  *mm; // 指向的是进程的虚拟内存,也就是载入资源和可执行文件的地方

	pid_t pid;   // 进程标识符,用来代表一个进程
   
    struct task_struct __rcu  *parent; // 指向父进程的指针
    struct list_head        children; // 子进程列表
   	struct list_head sibling;   // 兄弟进程

    struct fs_struct        *fs;  // 存放文件系统信息的指针
   
    struct files_struct     *files; // 一个数组,包含该进程打开的文件指针

    unsigned int policy; // 调度策略:一般有FIFO,RR,CFS
    ...
};

从2.6版本以后,Linux改用了slab分配器动态生成task_struct, 只需要在栈底(向下增长的栈)或栈顶(向上增长的栈)创建一个新的结构struct thread_info(这里是栈对象的尾端),你可以把slab分配器认为是一种分配和释放数据结构的优化策略。通过预先分配和重复使用task_struct, 可以避免动态分配和释放带来的资源消耗。

进程的地址空间 ref

所谓进程地址空间(process address space),就是从进程的视角看到的地址空间,是进程运行时所用到的虚拟地址的集合。
Linux——进程、线程_第1张图片

  • 32位系统的进程地址空间:虚拟地址空间的范围为 [公式] ,Linux系统将地址空间按3:1比例划分,其中用户空间(user space)占3GB,内核空间(kernel space)占1GB。虚拟地址空间和物理地址空间的转换如下图所示:
    Linux——进程、线程_第2张图片

进程的内存

Linux——进程、线程_第3张图片

程序段(Text):程序代码在内存中的映射,存放函数体的二进制代码。

初始化过的数据(Data):在程序运行初已经对变量进行初始化的数据。

未初始化过的数据(BSS):在程序运行初未对变量进行初始化的数据。

栈 (Stack):存储局部、临时变量,函数调用时,存储函数的返回指针,用于控制函数的调用和返回。在程序块开始时自动分配内存,结束时自动释放内存,其操作方式类似于数据结构中的栈。

堆 (Heap):存储动态内存分配,需要程序员手工分配,手工释放.注意它与数据结构中的堆是两回事,分配方式类似于链表。

注:1.Text, BSS, Data段在编译时已经决定了进程将占用多少VM

可以通过size,知道这些信息:

  1. 正常情况下,Linux进程不能对用来存放程序代码的内存区域执行写操作,即程序代码是以只读的方式加载到内存中,但它可以被多个进程安全的共享。

创建进程后都创建了哪些资源

进程创建

system() 通过调用shell启动一个新进程
exec() 以替换当前进程映像的方式启动一个新进程
fork() 以复制当前进程映像的方式启动一个新进程

fork(2)

执行fork后,父进程的task_struck对拷给子进程,父子进程最初资源完全一样,但是是两份不同的拷贝,因此任何改动都造成二者的分裂。
父子进程对内存资源(mm)的管理使用了COW(Copy-On-Write, 写时拷贝)技术:

  1. 在fork之前,一片内存区对应一份物理地址和一份虚拟地址,内存区的权限为RW;
  2. 在fork之后,父子进程看到的内存区虚拟地址相同,物理地址也相同,父子进程使用的其实是同一片物理内存,未发生内存拷贝,操作系统会将此内存区权限改为RO;
  3. 父或子进程对内存区执行写操作将触发PageFault,操作系统此时会将内存区拷贝一份,父子进程看到的虚拟地址仍然一样,但是物理地址已经不同。各进程虚拟地址到物理地址的映射由MMU(Memory Management Unit, 内存管理单元)管理。fork运行在有MMU的CPU上。
    Linux——进程、线程_第4张图片
    对于无MMU的CPU,无法应用COW,无法支持fork。无MMU的CPU使用vfork创建进程,父进程将一直阻塞直到子进程exit或exec。vfork和fork的本质区别是,vfork中的父子进程共用同一片内存区。

fork(2) 系统调用用于创建一个新进程,称为子进程,它与父进程同时运行(并发),且运行顺序不定(异步)。pid_t 是一个宏定义,其实质是 int,若成功则返回两个值,子进程返回 0,父进程返回子进程 ID;否则,出错返回 -1

父级的整个虚拟地址空间在子进程中复制,包括互斥锁的状态,

子进程与父进程完全相同,除了以下几点:

  • 子进程有自己唯一的进程号,这个进程号没有匹配任何现有进程组的 ID ( setpgid(2) ) 或会议。
  • 子进程的父进程ID与父进程ID相同
  • 子进程不继承父进程的内存锁( mlock(2) , mlockall(2) )
  • 进程资源利用率(getrusage(2))和 CPU 时间计数器(times(2))在孩子中被重置为零。
  • 孩子的待处理信号集最初是空的(sigpending(2))
  • 子进程不会从其继承信号量调整父(semop(2))
  • 子进程不继承进程相关的记录锁来自其父级 ( fcntl(2) )。(另一方面,它确实继承fcntl(2)打开文件描述锁和flock(2)从其父级锁定。)
  • 子进程没有从父进程继承定时器(setitimer(2)、alarm(2)、timer_create(2))
  • 子进程不继承未完成的异步 I/O(aio_read(3)、aio_write(3))的操作
  • 子进程的终止信号始终是SIGCHLD

进程回收

wait()waitpid()

通过 waitpid()/wait() 回收子进程的 task_struct 结构。

区别:

  • 阻塞非阻塞。
  • waitpid(-1, &status, 0) == wait(&status), wait() 是 waitpid() 的一个子集。

孤儿进程和僵尸进程

僵尸进程

僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。(子进程可以通过/proc/$pid看到,线程没有)
一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。
在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保 留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸。
如果他的父进程没安装SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。
但是如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程.

僵尸进程产生原因:

  1. 子进程结束后向父进程发出SIGCHLD信号,父进程默认忽略了它;
  2. 父进程没有调用wait()或waitpid()函数来等待子进程的结束。

避免僵尸进程方法:

  1. 父进程调用wait()/waitpid()等待子进程结束,这样处理父进程一般会阻塞在wait处而不能处理其他事情。
  2. 捕捉SIGCHLD信号,并在信号处理函数里面调用wait函数,这样处理可避免1中描述的问题。
  3. fork两次,父进程创建儿子进程,儿子进程再创建一个孙子进程,然后儿子进程自杀,孙子进程成为孤儿进程被init进程收养。

孤儿进程

孤儿进程:父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(pid=1)所收养,并由init进程对它们完成状态收集工作。(如果系统中出现孤儿进程,说明主进程在退出前没有清理子进程)

线程(Lightweight Process,LWP)

同一进程的多个线程获取进程ID时得到的是唯一ID值。Linux同一进程的多线程,在内核视角实际上每个线程都有一个PID,但在用户空间需要getpid()返回唯一值,Linux使用了一个小技巧,引入了TGID的概念,getpid()返回的的TGID值。

pthread_create()

Linux——进程、线程_第5张图片
Linux 线程本质上就是进程,只是与进程间资源共享方式不同,线程间共享所有资源。每个线程都有自己的task_struct,因此每个线程都可被CPU调度。多线程间又共享同一进程资源。

在一个线程中创建了另外一个线程,主线程要等到创建的线程返回了,获取该线程的返回值后主线程才退出。这个时候就需要用到线程挂起。pthread_join函数用于挂起当前线程,直至指定的线程终止为止。

说线程的PID,是指用户空间的进程ID,值就是TGID (thread group ID for the thread group leader);当特别指出,线程在内核空间的PID,则指线程在内核中task_struct里特有的PID。top –H 命令从线程视角显示CPU占用率。不带参数的top命令,进程ID是主线程的PID(也就是TGID)
Linux——进程、线程_第6张图片

Linux的进程和线程

  • 进程是资源分配的基本单位,线程是调度的基本单位
  • 进程是资源的集合,这些资源包括内存地址空间,文件描述符等等,一个进程中的多个线程共享这些资源。
  • 进程切换只发生在内核态,两步:
    切换页全局目录以安装一个新的地址空间;
    切换内核态堆栈和硬件上下文。
    另一种说法类似:
    保存CPU环境(寄存器值、程序计数器、堆栈指针);
    修改内存管理单元MMU的寄存器;
    转换后备缓冲器TLB中的地址转换缓存内容标记为无效。
  • CPU对任务进行调度时,可调度的基本单位 (dispatchable entity)是线程。如果一个进程中没有其他线程,可以理解成这个进程中只有一个主线程,这个主进程独享进程中的所有资源。
  • 进程的个体间是完全独立的,而线程间是彼此依存,并且共享资源。多进程环境中,任何一个进程的终止,不会影响到其他非子进程。而多线程环境中,父线程终止,全部子线程被迫终止(没有了资源)。
  • 系统调用fork创建一个进程时子进程是一段独立的内存空间,其中的资源是父进程资源的副本,两个进程是完全独立不共享内存资源的,二者需要通过IPC进行通信。
  • 线程上下文一般只包含CPU上下文及其他的线程管理信息。线程创建的开销主要取决于为线程堆栈的建立而分配内存的开销,这些开销并不大。线程上下文切换发生在两个线程需要同步的时候,比如进入共享数据段。切换只CPU寄存器值需要存储,并随后用将要切换到的线程的原先存储的值重新加载到CPU寄存器中去。

进程是处于运行期的程序和相关资源的总称,具备一些要素:

  1. 拥有一段可执行程序代码。就像一场戏需要一个剧本。代码段可以多个进程共用,就像许多场演出都可以用一份剧本一样。
  2. 拥有一段进程专用的系统堆栈空间。可以认为是这个进程的“私有财产”,相应的,也肯定有系统空间堆栈
  3. 在系统中有进程控制块(或称进程描述符, 本文两种说法通用)描述这个进程的相关信息。可以认为是进程的“户口”。系统通过这个控制块来控制进程的相关行为
  4. 有独立的存储空间,也就是专有的用户空间,相应的又会有用户空间堆栈。

理解各种 ID

# ps -eo ppid,pid,tid,lwp,tgid,pgrp,sid,tpgid,args -L | awk '{if(NR==1) print $0; if($9~/a.out/) print $0}'
   PPID     PID     TID     LWP    TGID    PGRP     SID   TPGID COMMAND
 579046 2436128 2436128 2436128 2436128 2436128  579046 2436128 ./a.out
 579046 2436128 2436129 2436129 2436128 2436128  579046 2436128 ./a.out
 579046 2436128 2436130 2436130 2436128 2436128  579046 2436128 ./a.out

pidstat -t [-p pid号] 可以打印出线程之间的关系。

各种ID最后都归结到pid和lwp(tid)上。所以理解各种ID,最终归结为理解pid和lwp(tid)的联系和区别。

PID: 进程ID。
LWP: 线程ID。在用户态的命令(比如ps)中常用的显示方式。
TID: 线程ID,等于LWP。TID在系统提供的接口函数中更常用,比如syscall(SYS_gettid)syscall(__NR_gettid)
TGID: 线程组ID,也就是线程组 leader的进程ID,等于 PID。
pgid (process group ID): 进程组ID,也就是进程组leader的进程ID。
pthread id: pthread库提供的ID,生效范围不在系统级别,可以忽略。
sid: session ID for the session leader。
TPGID: tty process group ID for the process group leader。
Linux——进程、线程_第7张图片
上图很好地描述了用户视角(user view)和内核视角(kernel view)看到线程的差别:

  • 从用户视角出发,在pid 42中产生的tid 44线程,属于tgid(线程组leader的进程ID) 42。甚至用ps和top的默认参数,你都无法看到tid 44线程。
  • 从内核视角出发,tid 42和tid 44是独立的调度单元,可以把他们视为"pid 42"和"pid 44"。
    Linux——进程、线程_第8张图片
  • 用户空间的线程 - - 负责执行线程的创建、销毁等操作
  • 内核空间的线程 - - 负责调度

协程

协程(用户级线程),这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户的程序自己调度的,因为是由用户程序自己控制,那么就很难像抢占式调度那样做到强制的CPU控制权切换到其他进程/线程,通常只能进行协作式调度,需要协程自己主动把控制权转让出去之后,其他协程才能被执行到。

goroutine 和协程区别

本质上,goroutine 就是协程。 不同的是,Golang 在 runtime、系统调用等多方面对 goroutine 调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前 goroutine 的CPU(P) 转让出去,让其他 goroutine 能被调度并执行,也就是 Golang 从语言层面支持了协程。

其他方面不同

  • 内存消耗方面
    每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少。
    goroutine: 2KB
    线程: 8MB
  • 线程/goroutine 切换(调度)开销方面
    线程/goroutine 切换开销方面,goroutine 远比线程小
    线程: 涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP…等寄存器的刷新等。
    goroutine: 只有三个寄存器的值修改 - PC / SP / DX.

进程调度

linux内核的三种主要调度策略:
1,SCHED_OTHER 分时调度策略,
2,SCHED_FIFO实时调度策略,先到先服务
3,SCHED_RR实时调度策略,时间片轮转
Linux——进程、线程_第9张图片

实时进程调度

SCHED_FIFO:不同优先级按照优先级高的先跑到睡眠,优先级低的再跑;同等优先级先进先出。
SCHED_RR:不同优先级按照优先级高的先跑到睡眠,优先级低的再跑;同等优先级轮转。
内核RT补丁:

如下两个参数
/proc/sys/kernel/sched_rt_period_us
/proc/sys/kernel/sched_rt_runtime_us
表示在period的时间里RT最多只能跑runtime的时间

普通进程调度

SCHED_OTHER:

CFS:完全公平调度(新内核)

红黑树,左边节点小于右边节点的值
运行到目前为止vruntime最小的进程
同时考虑了CPU/IO和nice
总是找vruntime最小的线程调度。
vruntime = pruntime/weight × 1024;
vruntime是虚拟运行时间,pruntime是物理运行时间,weight权重由nice值决定(nice越低权重越高),则运行时间少、nice值低的的线程vruntime小,将得到优先调度。这是一个随运行而动态变化的过程。

内核态与用户态

内核空间和用户空间

Linux的虚拟地址空间范围为0~4G,Linux内核将这4G字节的空间分为两部分, 将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各个进程使用,称为“用户空间。因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。

Linux使用两级保护机制:0级供内核使用,3级供用户程序使用,每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的,最高的1GB字节虚拟内核空间则为所有进程以及内核所共享。
内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。 虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000),另外, 使用虚拟地址可以很好的保护 内核空间被用户空间破坏,虚拟地址到物理地址转换过程有操作系统和CPU共同完成(操作系统为CPU设置好页表,CPU通过MMU单元进行地址转换)。

注:多任务操作系统中的每一个进程都运行在一个属于它自己的内存沙盒中,这个 沙盒就是虚拟地址空间(virtual address space),在32位模式下,它总是一个4GB的内存地址块。这些虚拟地址通过页表(page table)映射到物理内存,页表由操作系统维护并被处理器引用。每个进程都拥有一套属于它自己的页表。

进程内存空间分布如下图所示:
Linux——进程、线程_第10张图片

内存

堆向上增长,栈向下增长(为啥我的是相反的呢???)
堆栈、内存的增长方向与大小端

Linux——进程、线程_第11张图片
低    ->|-----------------|
      | 全局量(所有已初始化量 .data, |
      | 未初始化量 .bss )       |
  堆起始->|-----------------|
      |    堆向高地址增长      |
      |                 |
      |                 |
      |     自由空间        |
      |                 |
      |                 |
      |    栈向低地址增长      |
高 栈起始->|-----------------|

你可能感兴趣的:(Linux/Unit,编程基础,linux,运维,服务器)