工艺导论

  • 正在执行的程序或作业

  • 组件

    • 内存地址空间

    • 节目形象

    • 文件的句柄

    • 进程间通信

  • 每个进程都有一个父进程

    • Unix中最重要的进程是“init”

    • Windows排名靠前的大多数进程是:

      • 系统.exe-操作系统内核

      • Csrss.exe-处理用户模式Win32调用

      • Wininit.exe-管理系统服务

      • EXPLORER.EXE-操作系统Shell

      • Winlogin.exe-操作系统登录服务

进程内存布局

  • 程序的文本机器指令。

  • 数据初始化的静态数据和常量

  • BSS-未初始化的静态数据

  • 堆-进程的动态内存

    • 每个进程一个或多个

  • 堆栈-局部变量、调用堆栈、返回值

    • 对于内核模式线程,每个线程一个

    • 对于用户模式线程,每n个线程一个

  • 第一线程栈和堆的文本、数据、BSS和初始值的布局和分配由加载器负责

  • 堆栈和堆的管理是通过进程的运行时库和程序指令实现的

进程内存布局

内存布局

内存布局

多线程内存布局

多线程程序的内存布局

多线程程序的内存布局

预赛

  • 在|Systems-code-Examples-url|中维护的示例代码

  • 您可以使用|Systems-code-Examples-Clone|克隆到文件夹 systems-code-examples

  • 作为完整工作演示的所有后续示例将被引用为 systems-code-examples/<example-name>

  • 要运行示例,请确保您拥有 gcccmake ,以及 make 在你的电脑上。

  • 我们只在Ubuntu Linux、MacOS和Windows Subsystem for Linux2(带有Ubuntu 20.04LTS)上进行了测试。其他大多数人应该都能行得通。

检查流程布局示例

  • 我们的大多数示例都是用C编写的,并带有一些C++。

  • 获取代码

    git clone https://github.com/gkthiruvathukal/systems-code-examples cd systems-code-examples/c_intro

  • 生成 Makefile 使用 cmake **

    $ cd c_intro
    $ cmake .
    -- The C compiler identification is GNU 9.3.0
    -- The CXX compiler identification is GNU 9.3.0
    -- Check for working C compiler: /usr/bin/cc
    -- Check for working C compiler: /usr/bin/cc -- works
    -- Detecting C compiler ABI info
    -- Detecting C compiler ABI info - done
    -- Detecting C compile features
    -- Detecting C compile features - done
    -- Check for working CXX compiler: /usr/bin/c++
    -- Check for working CXX compiler: /usr/bin/c++ -- works
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Detecting CXX compile features
    -- Detecting CXX compile features - done
    >> Linux
    -- Configuring done
    -- Generating done
    -- Build files have been written to: /home/gkt/Work/systems-code-examples/c_intro
    
  • make 要将可执行文件装箱,请执行以下操作:

    $ make
    Scanning dependencies of target c-intro-demo
    [ 20%] Building CXX object CMakeFiles/c-intro-demo.dir/main.cc.o
    [ 40%] Building CXX object CMakeFiles/c-intro-demo.dir/debug.cc.o
    [ 60%] Building CXX object CMakeFiles/c-intro-demo.dir/list.cc.o
    [ 80%] Building CXX object CMakeFiles/c-intro-demo.dir/tests.cc.o
    [100%] Linking CXX executable bin/c-intro-demo
    [100%] Built target c-intro-demo
    
  • 请注意,对于我们的所有示例,输出可执行文件显示在 bin 子目录。

  • 还要注意,我们几乎所有的示例都使用 cmakemake 如图所示。

  • 运行 layout Shell脚本,它显示中的文本、数据和BSS的大小 bytes **

    $ ./layout
    
    section               size   addr
    .text                 3957    4352
    .data                   24   20480
    .bss                     8   20504
    

正在加载程序

  • 加载器为可执行文件的文本、数据、BSS、堆和堆栈分配内存。并将程序图像加载到内存中

  • 加载器从已在内存中分配共享库的操作系统获取信息,并加载尚未加载的共享库。每个共享库都有自己的文本、数据和BSS

  • 加载器遍历可执行文件并调整外部符号列表以指向内存中的正确位置(指向共享库)

  • 试试看 nm 命令查看编译后的对象/可执行文件中的符号。

  • 一旦程序准备好,加载程序就会调用 _start() 方法

  • _start() 打电话 _init() 对于每个共享库

  • _start() 初始化定义为全局变量的对象的静态构造函数

  • _start() 调用main(),程序开始

正在加载共享库(.so)

  • 类库也有数据、BSS和文本段

  • 共享库中的内存引用与位置无关(GCC -fpic-fPIC 旗帜A.新人GCC让PIC成为默认。使用 -fno-pic 如果你注意到这一点。

  • 链接器必须将所有这些与位置无关的内存访问解析为本地访问。这是通过为每个链接的进程编写GOT来实现的。

  • 需要与位置无关的存储器地址是因为将加载共享库的偏移量在同一程序的执行之间以及在其他程序之间将不同。

  • 此外,加载的相同共享库将与其他进程共享,而无需重新加载。因此,对于不同的程序,同一个库可能有不同的偏移量

更多关于GCC图片选项的信息

基于 man pagegcc

生成适合在共享库中使用的位置无关代码(PIC),如果目标计算机支持的话。这样的代码通过全局偏移表(GOT)访问所有常量地址。动态加载器在程序启动时解析GET条目(动态加载器不是GCC的一部分;它是操作系统的一部分)。如果链接的可执行文件的GET大小超过特定于计算机的最大大小,则会从链接器收到一条错误消息,指出-fpic不起作用;在这种情况下,请改用-fpic重新编译。(这些最大值在SPARC上为8k,在m68k和RS/6000上为32k。386没有这样的限制。)

正在加载共享库(.so)

  • 静态库不包含位置无关代码

  • 静态库只是未链接的 .o (对象)文件

  • 动态链接器只需将每个目标文件的文本、数据和BSS部分加载到程序的地址空间中

与职位无关的代码示例

  • 获取代码

    git clone https://github.com/gkthiruvathukal/systems-code-examples

    cd systems-code-examples/pic

  • 对于本例,您可以使用以下命令构建它 make -f Makefile.pic

  • 此示例的主要目的是显示生成PIC和非PIC之间的区别。

main.cc

Main.nopic.s-非位置独立码(GCC-fno-pic)

Main.nopic.s-位置独立代码(GCC-fPIC,默认选项)

有什么关系呢?

共享库-评估

优势

  • 减少了内存占用。如果两个程序加载相同的共享库,则由于GET,.Text段可以跨进程重复使用

弱点

  • 需要在操作系统中实施更高级的虚拟内存。对于简单或嵌入式系统有时并不实用

  • 需要更高级的编译器代码生成器。不同的处理器具有关于内存偏移量寄存器或函数表大小限制的特殊功能。

静态库-评估

优势

  • 当不希望重复使用时,这是有意义的。具有非常大的.data段的安装程序可执行文件就是一个很好的例子。

  • 第一次加载的时间比共享库更快。

  • 为GET查找生成的指令较少(小问题)

弱点

  • 更大的内存占用量。很少在应用程序之间重复使用公共代码。

库与静态链接程序

动态链接优势:

  • 内存占用

  • 代码重用

  • 使用新版本的共享库进行改进

  • 较小的可执行文件

静态链接的优势:

  • 在部署软件时,依赖项不那么重要(例如,缺少依赖项、错误升级依赖项、自定义补丁和对共享代码的更改)

  • 版本化和路径问题不那么重要

  • 代码模糊处理可以对对象文件进行模糊处理

  • 编译器优化器可以跨目标文件进行优化

进程保护

在具有虚拟内存和特权分离的现代操作系统中,提供以下保护:

  • 一个进程不能读取另一个进程的内存(除非明确允许)

  • 进程可以完全管理它可以访问的内存--垃圾收集、显式分配/释放、方法调用和参数传递标准、堆栈管理等。

  • 一个进程中的崩溃、异常、资源匮乏、死锁或其他错误不会直接影响其他进程

  • 当映射到相同的地址空间时,进程不能修改内核内存或以其他方式受操作系统保护的内存(如文本页)。

使用fork()创建进程

fork() 手册页:

  • fork() 通过复制调用进程来创建新进程。称为子进程的新进程与称为父进程的调用进程完全相同,但以下几点除外:

  • 子进程具有自己的唯一进程ID(ID)

  • 子进程的父进程ID与父进程的进程ID相同

  • 父级的线程不会在子级上重新创建

有趣的是:在Linux中, fork() != fork()fork() 打电话 clone()

从手册页:

  • fork() 将子进程ID返回父进程

  • fork() 将0返回子对象

  • fork() 如果无法创建子对象,则返回-1

Fork()示例

看见 systems-code-examples/fork 如果你想运行这个程序。

$ cd systems-code-examples/fork
$ cmake
$ make

使用克隆()创建进程

  • 类似于 fork() 因为创建了一个子进程。

  • clone() 允许与子进程共享父进程的不同部分

  • 用于创建轻量级进程(内核线程)的标志:

    • CLONE_FS -共享文件系统信息(chroot、chdir、umask.)

    • CLONE_FILES -共享文件描述符表

    • CLONE_SIGHAND -共享信号处理程序

    • CLONE_VM -共享页表

  • 还有更多的标志--不要忘记这个鲜为人知的功能!

  • Glibc版本的 fork() ,呼叫 clone() 没有任何这些旗帜

  • clone() 并非在所有的Unix操作系统中都存在(在Linux中可用,但不在Minix中提供)

Windows CreateProcess()和CreateThread()

  • 不同于Unix fork()/clone() -不共享部分流程

  • Windows有两种风格:

    • CreateProcess() -创建一个新进程,相当于调用 fork() 然后 execve() 在Unix中

    • CreateThread() -相当于创建 clone() 带有线程标志

  • 这是一种劣势吗?

    • 对于大多数用例和大多数程序,没有。

    • 绝大多数电话都是 clone() 在Unix中,它们相当于 CreateThread()

    • 绝大多数电话都是 fork() 在Unix中,它们相当于 CreateProcess()

在Windows上模拟fork()

一个著名的系统Cygwin实现了 fork() 在Windows上,如下所示:

  1. Cygwin.dll调用 CreateProcess() 创建挂起的子进程

  2. 父进程调用 setjmp() 保存寄存器

  3. 父进程将其BSS和数据段复制到子进程的地址空间。

  4. 父进程唤醒子进程并等待已命名的互斥体(互斥机制)。

  5. 孩子醒来,意识到这是一个分叉过程,然后远跳到保存的跳跃缓冲区。儿童解锁

  6. 父对象的命名互斥锁,并等待第二个互斥锁

  7. 父进程被唤醒,将其堆栈和堆复制到子进程。Release的子级命名互斥锁

  8. 子进程被唤醒,并将父进程通过共享内存向子进程发出的任何内存映射区域复制到子进程

  9. fork() Cygwin中的系统调用不使用写入时复制,而是使用“分叉时复制”。这类似于 fork() 早期Unix操作系统中的实现

进程终止的原因

  • 正常退出-返回自 main(...)

  • 错误退出-返回自 main(...) 带有错误代码

  • 致命错误

    • 段故障/总线错误-进程尝试读/写不可访问的内存或写入只读内存。

    • 堆栈溢出-堆栈指针增长到大于堆栈区域

    • 保护故障-尝试运行特权指令,如启用/禁用中断

    • 指令故障-除以零

  • 另一个进程通过信号或系统调用进行外部终止

WAIT()和WAITPID()示例

看见 systems-code-examples/wait 如果你想运行这个程序。