Linux进程与线程

8 篇文章 0 订阅

程序执行:
程序执行https://web.stanford.edu/class/cs101/software-1.html
进程与线程的主要区别:进程是资源(存储资源)分配的单位,而线程是task调度(cpu)的单位

进程

进程是资源分配的基本单位,是由于每个进程都有自己的虚拟内存空间,都是独立寻址。这也是为什么需要有进程间通信的机制。进程的地址空间结构:
进程地址空间
进程虚拟内存空间

  • 进程的创建:
    • 创建虚拟内存空间,初始化页表目录,用于将虚拟内存空间映射到物理内存
    • 读取可执行文件头,创建可执行文件与虚拟内存空间的映射关系,用于之后缺页的时候将文件从磁盘载入物理内存
    • 将cpu的指令寄存器设置为进程执行的入口地址(还涉及上下文切换等)

可执行文件与进程虚拟内存空间

  • 进程的执行
    • cpu开始执行指令后,发现入口地址指向的页面是一个空页,因此产生页错误(page fault),然后操作系统找到需要页面在的虚拟内存区域(VMA,可执行文件中的一个段segment),根据上面第二步建立的可执行文件和虚拟内存空间的映射关系,找到该页在文件中的偏移量,然后在物理内存中找到一个空页,并将该区域映射到物理内存中;然后操作系统将控制权交给进程,进程从刚才页错误的位置开始执行
    • 在执行过程中不断会产生页错误,因此不断重复上一个步骤

PS:segment是在装载过程中的一个概念,与section不同,section是链接的时候的概念。因为在装载过程中,由于页对齐,如果每个section都占用了一个页(有的section很小),会造成空间的浪费,因此,将可执行文件中的section根据访问权限(可读可执行,可读可写,只读)分成了不同的segment。这些segment的信息保存在可执行文件的program header中,可用readelf -l <可执行文件>查看,对于具体的进程可通过cat /proc/<pid>/maps查看虚拟空间分布;section的信息保存在section header中,可用readelf -S <可执行文件>查看

  • 在bash下执行ELF程序
    • 通过fork()(会调用clone()系统调用)创建子进程,然后新的进程调用execve()处理执行文件,原先的bash进程等待新进程结束
    • execve()系统调用后,内核开始装载ELF文件(对ELF文件进行映射,初始化ELF进程环境,设置系统调用的返回地址为ELF执行文件的入口地址)

线程

  • 线程的创建

    • 线程用pthread_create创建时,使用clone()系统调用,使用了与进程fork()调用clone()时不同的flag:CLONE_VM等,置上该flag后,线程与进程(主线程)使用相同的地址空间。
      对比两个程序:
      1: fork进程

      int main()
      {
          pid_t pid;
          pid = fork();
          if (pid == 0) {
              std::cout << "child process" << std::endl;
          } else {
              std::cout << "parent process" << std::endl;
          }
      }
      

      2: pthread_create线程

      void * run(void * args)
      {
          std::cout << "child thread" << std::endl;
          sleep(50);
      }
      int main()
      {
          pthread_t ptid1;
          pthread_create(&ptid1, nullptr, run, nullptr);
          pthread_join(ptid1, nullptr);
          return 0;
      }
      

      使用strace ./a.out执行这两个程序,可以发现:

      • fork程序的输出:
        clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fd3ecd58210) = 24203
      • pthread_create程序的输出:
        clone(child_stack=0x7efd47234fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7efd472359d0, tls=0x7efd47235700, child_tidptr=0x7efd472359d0) = 23882
        其中:
    标记含义
    CLONE_VM共享虚拟内存
    CLONE_FS共享与文件系统相关的属性
    CLONE_FILES共享打开文件描述符表
    CLONE_SIGHAND共享对信号的处置
    CLONE_THREAD置于父进程所属的线程组中
    • 线程模型

      Linux的线程模型有三种:N:1用户线程模型, 1:1内核线程模型和N:M混合线程模型
      用户级与内核级线程

      • N:1用户线程模型:LinuxThreads, 最早的模型, 只部分实现POSIX Threads标准

        完全由用户空间的程序库来创建,内核不知道线程的存在。内核仅为每个进程分配一个对应的内核线程,用于调度和陷入系统调用。由用户空间的库调度器来选择进程中的某个线程对应到该进程的内核线程,然后有操作系统调度到cpu运行。
        优点:进程中的线程切换方便
        缺点:一个进程同时只能有一个线程执行
        N-1线程模型

      • 1:1内核线程模型:NPTL, Native POSIX Thread Library, 从2.6内核以来到现在所使用的模型

        线程的创建,切换等由内核实现。每个用户线程都对应一个内核线程。内核空间内为每一个内核线程设置了一个线程控制块(TCB),内核根据该控制块,感知线程的存在,并进行控制。
        优点:内核能并行执行一个进程中的多个线程
        缺点:上下文切换复杂
        1-1线程模型
        Linux内核2.6以来的版本中,使用的pthread库就是使用的这种模型。pthread_create()创建的用户线程在有的文档也被称作轻量级进程(LWP,Light Weight Process)【LWP到底指的是用户线程,还是所对应的内核线程?两种方式都有文章说】。这是由于pthread_create在创建时与进程的创建一样,也使用了系统调用clone(), 只是在创建用户线程时,共享了进程(主线程)的虚拟内存空间。参考上面线程创建这一节。

        #include <iostream>
        #include <sys/types.h>
        #include <unistd.h>
        #include <pthread.h>
        
        void * run(void * args)
        {
            std::cout << "child thread" << std::endl;
            sleep(50);
        }
        
        int main()
        {
            pthread_t ptid1, ptid2, ptid3;
            pthread_create(&ptid1, nullptr, run, nullptr);
            pthread_create(&ptid2, nullptr, run, nullptr);
            pthread_create(&ptid3, nullptr, run, nullptr);
            std::cout << "main thread" << std::endl;
            pthread_join(ptid1, nullptr);
            pthread_join(ptid2, nullptr);
            pthread_join(ptid3, nullptr);
            return 0;
        }
        

        执行上面的例子,用命令ps -eLf查看:
        线程LWP
        其中LWP表示轻量级进程的pid,NLWP为线程组中线程的个数。可以看到a.out的4个线程(一个主线程)的pid都是4289,说明这几个线程是并列的关系;ppid都为29129表明线程之间没有父子关系;每个线程都和一个LWP对应,分别是4289,4290,4291,4292;这4个线程形成一个线程组,因此NLWP为4.

      • N:M混合线程模型:NGPT, Next Generation POSIX Threads, 已终止

      N-M线程模型

    PS: 为什么是内核线程,而不是内核进程:由于内核线程运行在内核,共享同一个内核空间,内核页表。

    • 线程栈
      每个线程创建之后有自己独有的线程栈。不像进程栈的大小是动态增长的,线程栈的栈大小是固定的,每个线程栈都有一定大小的边界,一旦线程访问超过了边界就会报错。从这个线程栈可以分配在靠近堆的位置(堆顶附近自顶向下),也可以分配在靠近栈的位置(自己执行出来的与参考文档中的不一样)。
      1. 在堆附近分配

        执行ulimit -a查看栈大小:

        执行线程模型中的代码, 通过cat /proc/<pid>/maps查看进程的地址空间。
        可以看到heap下面,除了主线程的栈,还有3个线程栈及其边界:

        在这里插入图片描述

      2. 在栈附近分配
        ulimit -s unlimited 设置 stack size 为 unlimited,注意虽然设置了stack size为无限,但是实际上其并不是无限的,而也是固定大小的线程栈。
        执行线程模型中的代码, 通过cat /proc/<pid>/maps查看进程的地址空间。
        发现是共享内存区域,栈,堆,代码区,数据区的相对位置都发生了变化。
        在这里插入图片描述
        在这里插入图片描述
        不过可以确定的是,每个线程都有自己的线程栈,并且线程栈的大小是固定的。

        PS:用户栈与内核栈 - 每个进程有2个栈,用户栈和内核栈,在进程系统调用或者中断的时候,会陷入内核态,此时,会将用户态的堆栈保存在内核栈中,然后将堆栈指针变为内核栈的地址。由于进程从用户态转到内核态时,内核栈总是空的,所以可以直接将内核栈的栈顶地址保存在堆栈指针寄存器中。同时,这里说的进程是指只有一个主线程。进程与线程并不共享一个内核栈(也许每个LWP有一个内核栈?)。

CPU上下文切换

  • 进程上下文切换:
    1.页表 – 对应虚拟内存资源
    2.文件描述符表/打开文件表 – 对应打开的文件资源
    3.寄存器 – 对应运行时数据
    4.信号控制信息/进程运行信息
  • 线程上下文切换 - 同一个进程中:
    1.栈
    2.寄存器 – 对应运行时数据

PS: 文章是参考多篇文章写的,由于没有看过Linux的内核源码,可能理解有错误的地方,请大家帮忙指出来。

  1. Linux可执行文件与进程的虚拟地址空间:https://cloud.tencent.com/developer/article/1624718
  2. 进程地址空间,堆和栈关系:https://blog.csdn.net/icandoit_2014/article/details/56666569
  3. 进程与线程的区别:https://harmonyos.51cto.com/posts/659
  4. 进程在cpu的执行过程:https://blog.csdn.net/hunter___/article/details/82906540
  5. 内核-linux进程切换:https://r00tk1ts.github.io/2017/08/26/Linux%E8%BF%9B%E7%A8%8B%E5%88%87%E6%8D%A2/
  6. Linux进程线程相关概念梳理:https://segmentfault.com/a/1190000019066170
  7. 线程的三种实现方式:https://blog.csdn.net/gatieme/article/details/51892437
  8. posix线程:https://cloud.tencent.com/developer/article/1008758
  9. 内核线程,轻量级进程,用户线程概念解惑:https://blog.csdn.net/gatieme/article/details/51481863
  10. pthread库创建的线程属于用户级线程还是内核级线程:https://www.zhihu.com/question/35128513
  11. 普通线程与内核线程:https://www.cnblogs.com/alantu2018/p/8526916.html
  1. 用户栈与内核栈有什么区别:https://www.jianshu.com/p/6b2ec520ae02
  2. 线程与栈:https://network.51cto.com/art/202006/619565.htm
  3. 线程栈如何分配:https://ahoj.cc/2020/03/%E3%80%90%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E3%80%91%E7%BA%BF%E7%A8%8B%E6%A0%88%E5%A6%82%E4%BD%95%E5%88%86%E9%85%8D/
  4. Linux中的各种栈:进程栈 线程栈 内核栈 中断栈:https://juejin.cn/post/6844903686003490830
  5. 用户栈和内核栈的区别:https://www.jianshu.com/p/6b2ec520ae02
  6. 用户栈/内核栈:https://www.huaweicloud.com/articles/359d7c5a96089200119047b61da76098.html
  7. 一文让你明白CPU上下文切换:https://zhuanlan.zhihu.com/p/52845869
  8. linux线程切换和进程切换的方法
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值