操作系统概述(三、虚拟化)

系列文章目录


文章目录


前言

十一、操作系统上的进程

操纵系统内核的启动:CPU Reset->Firmware->Boot loader->Kernel_start()->...

init进程, init进程通过系统调用创建Linux中的所有...

  • 操作系统启动后做了什么?
  • 操作系统如何管理程序(进程)

1. 从系统启动到第一个进程

eg:

c 复制代码
int main()
{
  cte_init(on_interrupt); // 注册中断

  for (int i = 0; i < LENGTH(tasks); ++i){
    // 启动线程,某些mcu甚至没有内存虚拟化,只能启动简单的线程
  }
  mpe_init(); // 进入系统内核
}

操作系统启动后会加载"第一个程序",之后Linux kernel进入后台成为"中断/异常处理程序"。

程序:状态机

  • c代码视角:语句
  • 汇编/机器代码视角:指令
  • 与操作系统交互的方式:syscall

syscall: 进程管理,内存管理,文件管理...

系统调用:fork(), 创建进程

调用系统API, fork将当前状态复制

c 复制代码
/* Clone the calling process, creating an exact copy.
   Return -1 for errors, 0 to the new process,
   and the process ID of the new process to the old process.  */
extern __pid_t fork (void) __THROWNL;

操作系统:状态机的管理者

虚拟化:操作系统可以同时管理多个状态机,每一步选择一个状态执行

c 复制代码
#include <unistd.h>
#include <stdio.h>

int main()
{
  pid_t pid1 = fork();
  pid_t pid2 = fork();
  pid_t pid3 = fork();
  printf("Hello World from (%d, %d, %d)\n", pid1, pid2, pid3); 
}
c 复制代码
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>

int main(int argc, const *argv[])
{
  for (int i = 0; i < 2; ++i) {
    fork();
    printf("Hello\n");
  }
  for (int i = 0; i < 2; ++i) {
    wait(nullptr);
  }
}

./a.out 打印6个Hello,但是

./a.out | cat 以及 ./a.out | cat 显示8个Hello

需要在开头加入 setbuf(stdout, nullptr); 不缓冲标准输出

解释:

cpp 复制代码
printf("Hello");  // fflush(stdout);
int *ptr = nullptr;
*ptr = 1;

若无 // 程序报错时不会输出hello,若在输出前清空缓冲区则在报错前会输出hello

printf(); 会缓冲标准输出,fork时缓冲区的内容也会同步复制,因此./a.out | wc -l 为8

execv()

c 复制代码
/* Replace the current process, executing PATH with arguments ARGV and
   environment ENVP.  ARGV and ENVP are terminated by NULL pointers.  */
extern int execve (const char *__path, char *const __argv[],
		   char *const __envp[]) __THROW __nonnull ((1, 2));

重置状态机,

shell 复制代码
man execve

bash -c env  # 显示当前环境变量
cpp 复制代码
#include <unistd.h>
#include <stdio.h>

// int main(int argc, char* argv[], char* envp[]);
int main()
{
  // nullptr 为手册中规定的必填项
  char*  argv[] = {"/bin/bash", "-c", "env", nullptr};
  char*  envp[] = {"HELLO=WORLD", nullptr};
  execve(argv[0], argv, envp);  // 重置状态机
  printf("Hello, World!\n");
}

常用环境变量:

export HELLO=WORLD

so:

env | grep HELLO

get: HELLO=WORLD

PATH环境变量

shell 复制代码
PATH=x:y:z gcc test.c  # 改变gcc环境变量从而到会不能正常运行

销毁进程

销毁状态机

return;

void exit(int status); // 进程normal terminate,将 status 返回给父进程

_exit();

syscall(SYS_exit, 0);

cpp 复制代码
#include <stdlib.h>
#include <stdio.h>
void func()
{
  printf("Goodbye, OS\n");
}
int main(int argc, char* argv[])
{
  atexit(func);
  // main函数返回,退出进程
  if (argc < 2) return EXIT_FAILURE;
  // 退出进程
  if (strcmp(argv[1], "exit") == 0) exit(0);
  // 退出进程,不调用func
  if (strcmp(argv[1], "_exit") == 0) _exit(0);
  // 强行退出当前线程,不安全,不会析构对象
  if (strcmp(argv[1], "__exit") == 0) syscall(SYS_exit, 0);
}

详情:

shell 复制代码
man exit
man _exit
man syscall

十二、进程的地址空间

c代码:堆、栈、内核区

汇编代码: 地址空间+寄存器

char *p可以和intptr_t互相转换

  • 可以指向"任何地方"
  • 合法的地址(可读或可写)
    • 代码(main, %rip会从此处取出待执行的指令)只读
    • 数据(static int x), 读写
    • 堆栈(int y), 读写
    • 运行时分配的内存,读写
    • 动态链接库
  • 非法的地址
    • NULL, 导致segmentation fault

查看进程的地址空间

32085:   ./a.out
0000561407b26000      4K r---- a.out
0000561407b27000      4K r-x-- a.out
0000561407b28000      4K r---- a.out
0000561407b29000      4K r---- a.out
0000561407b2a000      4K rw--- a.out
00005614097ac000    132K rw---   [ anon ]
00007f9754000000    132K rw---   [ anon ]
00007f9754021000  65404K -----   [ anon ]
00007f975bbfe000      4K -----   [ anon ]
00007f975bbff000   8192K rw---   [ anon ]
00007f975c3ff000      4K -----   [ anon ]
00007f975c400000   8192K rw---   [ anon ]
00007f975cc00000    160K r---- libc.so.6
00007f975cc28000   1620K r-x-- libc.so.6
00007f975cdbd000    352K r---- libc.so.6
00007f975ce15000     16K r---- libc.so.6
00007f975ce19000      8K rw--- libc.so.6
00007f975ce1b000     52K rw---   [ anon ]
00007f975d000000    616K r---- libstdc++.so.6.0.30
00007f975d09a000   1088K r-x-- libstdc++.so.6.0.30
00007f975d1aa000    444K r---- libstdc++.so.6.0.30
00007f975d219000     44K r---- libstdc++.so.6.0.30
00007f975d224000     12K rw--- libstdc++.so.6.0.30
00007f975d227000     12K rw---   [ anon ]
00007f975d2fc000     16K rw---   [ anon ]
00007f975d300000     56K r---- libm.so.6
00007f975d30e000    496K r-x-- libm.so.6
00007f975d38a000    364K r---- libm.so.6
00007f975d3e5000      4K r---- libm.so.6
00007f975d3e6000      4K rw--- libm.so.6
00007f975d3e7000     12K r---- libgcc_s.so.1
00007f975d3ea000     92K r-x-- libgcc_s.so.1
00007f975d401000     16K r---- libgcc_s.so.1
00007f975d405000      4K r---- libgcc_s.so.1
00007f975d406000      4K rw--- libgcc_s.so.1
00007f975d419000      8K rw---   [ anon ]
00007f975d41b000      8K r---- ld-linux-x86-64.so.2
00007f975d41d000    168K r-x-- ld-linux-x86-64.so.2
00007f975d447000     44K r---- ld-linux-x86-64.so.2
00007f975d453000      8K r---- ld-linux-x86-64.so.2
00007f975d455000      8K rw--- ld-linux-x86-64.so.2
00007fffca336000    132K rw---   [ stack ]
00007fffca3a6000     16K r----   [ anon ]
00007fffca3aa000      8K r-x--   [ anon ]
ffffffffff600000      4K --x--   [ anon ]

pmap(1) - report memory of a process

  • Claim: pmap是通过访问procfs(/proc/)实现的
    查看进程的地址空间
  • 进程的地址空间:若干连续的"段"
  • "段"的内存可以访问
  • 不在段内/违反权限的内存访问触发SIGSEGV
    • gdb可以"越权访问",但不能访问"不存在"的地址

在不进入系统内核的情况下完成系统调用

例子:

  • time,时间:内核维护秒级的时间(所有进程映射同一个页面)
  • 例子:gettimeofday
    • RTFSC
  • RTFM

进程地址空间管理

c 复制代码
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags,
                                         int fd, off_t offset);
int munmap(void *addr, size_t length);

将文件映射到进程地址空间

进程地址空间隔离

进程内的地址和数据只能访问本进程的地址空间

通过open("/proc/pid/mem, xxx);可访问其他进程的内存空间

十三、系统调用和 shell

shell提供用户接口

  • "与人类直接交互的第一个程序"
  • 帮助人类创建/管理进程(应用程序)、数据文件...

如:

  • "Command-line interface"
  • shell 是一门"把用户指令翻译成系统调用"的编程语言
  • man sh(推荐阅读!), bash ...
  • Graphical Shell(GUI)如Windows,Symbian,Android...

shell 常用命令:

  • 重定向 ls > a.txt
  • 管道 ls >| wc -l
  • 后台 ls &
  • 命令组合 (echo a; echo b) | wc -l

A Zero-dependency UNIX Shell

  • 零库函数依赖(-ffreestanding编译、ld链接)
  • 可以作为最小Linux的init程序
  • 用到 文件描述符:一个打开文件的"指针"

如何阅读A Zero-dependency UNIX Shell的代码?

  • strace + gdb

管道的运行原理? easy and clear

十四、C标准库的实现

libc:

如何实现 printf(const char* fmt, ...); 可变参数

fd, execve等封装

stdio.h

FILE* 背后其实是是个文件描述符

  • 可以用gdb查看具体的FILE* (如stdout)
  • 可以看到 glibc 的一些内部实现
  • 可以加载glibc的 debugs symbols
  • 封装了文件描述符上的系统调用(fseek, fgetpos, ftell, feof)
cpp 复制代码
// 更易用的封装接口
execlp("echo", "hello", "world", nullptr);

环境变量:env, bash -c env

实现:打印环境变量的小程序:

cpp 复制代码
#include <stdio.h>
int main()
{
  // Q? environ 是如何被赋值的?
  extern char** environ;
  for (char** env = environ; *env; env++) {
    printf("%s\n", *env);
  }
}

编译:
gcc env.c -g -static 静态链接
gcc env.c -g 动态链接

gdb a.out
p (char**)environ

starti

wa (char**)environ

c

内存管理封装

malloc 和 free

  • 在大区间[L, R) 中维护不相交的区间集合
    M = {[l0, r0), [l1, r1), ...}
  • malloc(s) - 返回一段大小未 s 的区间
    • 必要时可以向操作系统申请额外的[L, R) (观察strace)
    • 允许在内存不足时"拒绝"请求
  • free(l) - 给定l 删除 {l, r) ∈ M

Premature optimization is the root of all evil. -- D.E.Knuth

workload? 合理假设

  • 越小的对象创建/分配越频繁
  • 较为频繁地分配中等大小的对象
  • 低频率的大对象分配
  • 满足并行要求,即每个线程都会"同时"分配内存
    设置两套系统:
  • fast path
    • 性能极好、并行度极高、覆盖大部分情况
    • 但有小概率会失败
  • slow path
    • 不在乎那么快
    • 但把困难的事情做好
      • 计算机系统里有很多这样的例子(比如cache)

参考 STL 的二级空间分配器

先用锁分配大块内存,然后再把大块内存切成同等小块的分配

十五、fork

概念

系统调用 -> libc -> shell -> 应用软件栈

fd本质是int型,它像一个指针,指向了操作系统的对象。

在调用execve时,fd不会重置!

int open(const char* pathname, int flags);

  • RTFM:O_CLOEXEC, O_APPEND
    文件描述符:一个指向操作系统内对象的"指针"
  • 对象只能通过操作系统允许的方式访问
  • 从0开始编号(0,1,2...stdin,stdout,stderr)
  • 可以通过open取的;close释放;dup"复制"
  • 对于数据文件,文件描述符会"记住"上次访问文件的位置
    • write(3, "a", 1); write(3, "b", 1);

fork下的文件描述符

c 复制代码
fd = open("a.txt", O_WRONLY | O_CREAT); assert(fd > 0);
int pid = fork(); assert(pid > 0);
if (pid == 0) {
  write(fd, "Hello", 5);
} else {
  write(fd, "World", 5);
}

在dup时,不同的fd号指向同一个对象,同时也共享offset

fork的实现

mmu分页

进程所有的页面属于操作系统,进程只拥有映射表

page fault 缺页错误

copy on write

  • "Copy-on-write"只有被写入的页面才会复制一份
    • 被复制后,整个地址空间都被标记为"只读"
    • 操作系统捕获Page Fault后酌情复制页面
    • fork-execve效率得到提升
  • 操作系统会维护每个页面的引用计数
cpp 复制代码
申请内存,malloc(128M);
for(int i=0; i<1000; ++i) {
  if (pid == 0) break;
}
使用内存
  • 所以,真个操作系统里libc代码和只读数据只有一个副本!
  • 推论:统计进程占用的内存是个伪命题

所以一个进程占用了多少内存如何计算?

用虚拟内存查看内存泄露

other

利用fork实现回溯

dfs_fork

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>

#define DEST  '+'
#define EMPTY '.'

struct move {
  int x, y, ch;
} moves[] = {
  { 0, 1, '>' },
  { 1, 0, 'v' },
  { 0, -1, '<' },
  { -1, 0, '^' },
};

char map[][512] = {
  "#######",
  "#.#.#+#",
  "#.....#",
  "#.....#",
  "#...#.#",
  "#######",
  "",
};

void display();

void dfs(int x, int y) {
  if (map[x][y] == DEST) {
    display();
  } else {
    int nfork = 0;

    for (struct move *m = moves; m < moves + 4; m++) {
      int x1 = x + m->x, y1 = y + m->y;
      if (map[x1][y1] == DEST || map[x1][y1] == EMPTY) {
        int pid = fork(); assert(pid >= 0);
        if (pid == 0) { // map[][] copied
          map[x][y] = m->ch;
          dfs(x1, y1);
          exit(0); // clobbered map[][] discarded
        } else {
          nfork++;
          waitpid(pid, NULL, 0); // wait here to serialize the search
        }
      }
    }

    while (nfork--) wait(NULL);
  }
}

int main() {
  dfs(1, 1);
}

void display() {
  for (int i = 0; ; i++) {
    for (const char *s = map[i]; *s; s++) {
      switch (*s) {
        case EMPTY: printf("   "); break;
        case DEST : printf(" ○ "); break;
        case '>'  : printf(" → "); break;
        case '<'  : printf(" ← "); break;
        case '^'  : printf(" ↑ "); break;
        case 'v'  : printf(" ↓ "); break;
        default   : printf("▇▇▇"); break;
      }
    }
    printf("\n");
    if (strlen(map[i]) == 0) break;
  }
  fflush(stdout);
  sleep(1); // to see the effect of parallel search
}

应用

跳过初始化

  • Zygote Process (Android)
    • Java Virtual Machine 初始化涉及大量的类加载
    • 一次加载,全员使用
      • App 使用的系统资源
      • 基础类库
      • libc
      • ...
  • Chrome site isolation (Chrome)
  • Fork server (AFL)

利用fork保存当前程序快照,从而记录程序运行中的某种状态

弊端

  • 如果只有内存和文件描述符,没问题
  • 信号
  • 线程
  • 进程间通讯对象
  • ptrace(追踪/调试)

so:

c 复制代码
int posix_spawn(pid_t *pid, char*path, 
  posix_spawn_file_actions_t *file_actions, 
  posix_spawnattr_t *attrp, 
  char* argv[], char* envp[]);

十六、可执行文件

  • 可执行文件
  • 解析可执行文件
  • 链接和加载
    • 假设只有静态链接

手册:

System V ABI: System V Application Binary Interface

file a.out

a.out: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=b6e0470ba2180936a61ec7ab71131a73121f88ee, for GNU/Linux 3.2.0, not stripped

可执行文件:状态机的描述

  • 可执行文件是最重要的操作系统对象

  • 描述了状态机的初始状态+迁移的数据结构

    • 寄存器+内存

      strace a.out

      execve("./a.out", ["./a.out"], 0x7ffe0d273c20 /* 63 vars /) = 0
      arch_prctl(0x3001 /
      ARCH_??? /, 0x7ffc6f6cbdc0) = -1 EINVAL (Invalid argument)
      brk(NULL) = 0xd62000
      brk(0xd62dc0) = 0xd62dc0
      arch_prctl(ARCH_SET_FS, 0xd623c0) = 0
      set_tid_address(0xd62690) = 7873
      set_robust_list(0xd626a0, 24) = 0
      rseq(0xd62d60, 0x20, 0, 0x53053053) = 0
      uname({sysname="Linux", nodename="MyComputer", ...}) = 0
      prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192
      1024, rlim_max=RLIM64_INFINITY}) = 0
      readlink("/proc/self/exe", "/home/sdf/a.out", 4096) = 15
      getrandom("\x81\x04\x29\xc6\xd5\x76\x65\x0f", 8, GRND_NONBLOCK) = 8
      brk(0xd83dc0) = 0xd83dc0
      brk(0xd84000) = 0xd84000
      mprotect(0x4c1000, 16384, PROT_READ) = 0
      exit_group(0) = ?
      +++ exited with 0 +++

      strace ./a.c

      execve("./a.c", ["./a.c"], 0x7ffe85525b00 /* 63 vars */) = -1 ENOEXEC (Exec format error)
      strace: exec: Exec format error
      +++ exited with 1 +++

  • Linux

    • a.out(deprecated)

    • ELF(Executable Linkable Format)

    • Shell-bang

      • Shell-bang 其实是一个"偷换参数"的execve
      • 当加载器读取到#!后会将其后的参数传入execve
      shell 复制代码
      #! magic number

解析可执行文件

https://www.gnu.org/software/binutils/

GNU binutils

  • 生成可执行文件
    • ld(linker), as(assembler)
    • ar, ranlib
  • 分析可执行文件
    • objcopy/objdump/readelf
    • addr2line, size, nm

gdb core

bt/backtrace

如何追踪到程序挂掉时的调用栈?

函数调用call时会在栈上留下endbr,然后push %rbp,然后mov %rsp,%rbp

逆向工程(Reverse Engineering)

from C code to binary

cpp 复制代码
int main()
{
  hello();
}
cpp 复制代码
void hello()
{
  char* p = (char*)main + 0xa + 1;
  int32_t offset = *(int32_t*)p;
  assert( (char*)main + 0xf + offset == (char*)hello );
  // (char*)main + 0xf // call hello的next PC 
}

重新理解编译、链接

连接器(ld)将所有的符号链接

十七、可执行文件的加载

  • 若干真正的静态ELF加载器
  • 动态链接和加载

ELF loader on OS

可执行文件

  • 一个描述了状态机的初始状态(迁移)的数据结构
    • 不同于内存里的数据结构,"指针"都被"偏移量"代替
    • 数据结构各个部分定义:/usr/include/elf.h

加载器(loader)

  • 解析数据结构+复制到内存+跳转
  • 创建进程运行时初始状态(argv, evnp, ...)
    • loader-static.c
      • 可以加载任何静态链接的代码 minimal.S, dfs-fork.c
      • 并且能正确处理参数/环境变量 env.c
    • RTFM:

loader-static.c

cpp 复制代码
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <elf.h>
#include <fcntl.h>
#include <sys/mman.h>

#define STK_SZ           (1 << 20)
#define ROUND(x, align)  (void *)(((uintptr_t)x) & ~(align - 1))
#define MOD(x, align)    (((uintptr_t)x) & (align - 1))
#define push(sp, T, ...) ({ *((T*)sp) = (T)__VA_ARGS__; sp = (void *)((uintptr_t)(sp) + sizeof(T)); })

void execve_(const char *file, char *argv[], char *envp[]) {
  // WARNING: This execve_ does not free process resources.
  int fd = open(file, O_RDONLY);
  assert(fd > 0);
  Elf64_Ehdr *h = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0);
  assert(h != (void *)-1);
  assert(h->e_type == ET_EXEC && h->e_machine == EM_X86_64);

  Elf64_Phdr *pht = (Elf64_Phdr *)((char *)h + h->e_phoff);
  for (int i = 0; i < h->e_phnum; i++) {
    Elf64_Phdr *p = &pht[i];
    if (p->p_type == PT_LOAD) {
      int prot = 0;
      if (p->p_flags & PF_R) prot |= PROT_READ;
      if (p->p_flags & PF_W) prot |= PROT_WRITE;
      if (p->p_flags & PF_X) prot |= PROT_EXEC;
      void *ret = mmap(
        ROUND(p->p_vaddr, p->p_align),              // addr, rounded to ALIGN
        p->p_memsz + MOD(p->p_vaddr, p->p_align),   // length
        prot,                                       // protection
        MAP_PRIVATE | MAP_FIXED,                    // flags, private & strict
        fd,                                         // file descriptor
        (uintptr_t)ROUND(p->p_offset, p->p_align)); // offset
      assert(ret != (void *)-1);
      memset((void *)(p->p_vaddr + p->p_filesz), 0, p->p_memsz - p->p_filesz);
    }
  }
  close(fd);

  static char stack[STK_SZ], rnd[16];
  void *sp = ROUND(stack + sizeof(stack) - 4096, 16);
  void *sp_exec = sp;
  int argc = 0;

  // argc
  while (argv[argc]) argc++;
  push(sp, intptr_t, argc);
  // argv[], NULL-terminate
  for (int i = 0; i <= argc; i++)
    push(sp, intptr_t, argv[i]);
  // envp[], NULL-terminate
  for (; *envp; envp++) {
    if (!strchr(*envp, '_')) // remove some verbose ones
      push(sp, intptr_t, *envp);
  }
  // auxv[], AT_NULL-terminate
  push(sp, intptr_t, 0);
  push(sp, Elf64_auxv_t, { .a_type = AT_RANDOM, .a_un.a_val = (uintptr_t)rnd } );
  push(sp, Elf64_auxv_t, { .a_type = AT_NULL } );

  asm volatile(
    "mov $0, %%rdx;" // required by ABI
    "mov %0, %%rsp;"
    "jmp *%1" : : "a"(sp_exec), "b"(h->e_entry));
}

int main(int argc, char *argv[], char *envp[]) {
  if (argc < 2) {
    fprintf(stderr, "Usage: %s file [args...]\n", argv[0]);
    exit(1);
  }
  execve_(argv[1], argv + 1, envp);
}

boot main

cpp 复制代码
#include <stdint.h>
#include <elf.h>
#include <x86/x86.h>

#define SECTSIZE 512
#define ARGSIZE  1024

static inline void wait_disk(void) {
  while ((inb(0x1f7) & 0xc0) != 0x40);
}

static inline void read_disk(void *buf, int sect) {
  wait_disk();
  outb(0x1f2, 1);
  outb(0x1f3, sect);
  outb(0x1f4, sect >> 8);
  outb(0x1f5, sect >> 16);
  outb(0x1f6, (sect >> 24) | 0xE0);
  outb(0x1f7, 0x20);
  wait_disk();
  for (int i = 0; i < SECTSIZE / 4; i ++) {
    ((uint32_t *)buf)[i] = inl(0x1f0);
  }
}

static inline void copy_from_disk(void *buf, int nbytes, int disk_offset) {
  uint32_t cur  = (uint32_t)buf & ~(SECTSIZE - 1);
  uint32_t ed   = (uint32_t)buf + nbytes;
  uint32_t sect = (disk_offset / SECTSIZE) + (ARGSIZE / SECTSIZE) + 1;
  for(; cur < ed; cur += SECTSIZE, sect ++)
    read_disk((void *)cur, sect);
}

static void load_program(uint32_t filesz, uint32_t memsz, uint32_t paddr, uint32_t offset) {
  copy_from_disk((void *)paddr, filesz, offset);
  char *bss = (void *)(paddr + filesz);
  for (uint32_t i = filesz; i != memsz; i++) {
    *bss++ = 0;
  }
}

static void load_elf64(Elf64_Ehdr *elf) {
  Elf64_Phdr *ph = (Elf64_Phdr *)((char *)elf + elf->e_phoff);
  for (int i = 0; i < elf->e_phnum; i++, ph++) {
    load_program(
      (uint32_t)ph->p_filesz,
      (uint32_t)ph->p_memsz,
      (uint32_t)ph->p_paddr,
      (uint32_t)ph->p_offset
    );
  }
}

static void load_elf32(Elf32_Ehdr *elf) {
  Elf32_Phdr *ph = (Elf32_Phdr *)((char *)elf + elf->e_phoff);
  for (int i = 0; i < elf->e_phnum; i++, ph++) {
    load_program(
      (uint32_t)ph->p_filesz,
      (uint32_t)ph->p_memsz,
      (uint32_t)ph->p_paddr,
      (uint32_t)ph->p_offset
    );
  }
}

void load_kernel(void) {
  Elf32_Ehdr *elf32 = (void *)0x8000;
  Elf64_Ehdr *elf64 = (void *)0x8000;
  int is_ap = boot_record()->is_ap;

  if (!is_ap) {
    // load argument (string) to memory
    copy_from_disk((void *)MAINARG_ADDR, 1024, -1024);
    // load elf header to memory
    copy_from_disk(elf32, 4096, 0);
    if (elf32->e_machine == EM_X86_64) {
      load_elf64(elf64);
    } else {
      load_elf32(elf32);
    }
  } else {
    // everything should be loaded
  }

  if (elf32->e_machine == EM_X86_64) {
    ((void(*)())(uint32_t)elf64->e_entry)();
  } else {
    ((void(*)())(uint32_t)elf32->e_entry)();
  }
}

linux 内核源码

  • 解压
  • make menuconfig (生成.config文件)
  • make bzImage -j8 (生成镜像???)

编译结果

  • vmlinux(ELF格式的内核二进制代码)
  • vmlinuz ( 压缩的镜像,可以直接被QEMU加载 )
  • readelf入口地址0x1000000(物理内存16M位置)

动态链接

...

存储保护和加载位置

  • 允许将.dl中的一部分以某个指定的权限映射到内存的某个位置(program header table)
  • 允许自由指定加载器
  • 加入INTERP

空间浪费

  • 字符串存储在常量池,统一通过"指针"访问

--

  • "符号表"就是Global Offset Table(GOT)
  • 增加一层indirection: Procedure Linkage Table(PLT)
  • 所有未解析的符号都统一翻译成call

十八、XV6

source code:
xv6-riscv

xv6: UNIX v6的现代"克隆"

接近完整的UNIX Shell体验

  • 基本工具集(wc, echo, cat, ...)
  • 命令执行、管道、重定向
    • 支持多处理器
    • Now in RISC-V
  • 通过这些系统调用足够支撑以下应用
    • cc, as, ld, vi, sed, awk, troff, lp, ...

当拿到makefile时:

  1. make -nB qemu
  2. 想要知道所有命令调用序列
    make -nB qemu | vim -
    :set nowrap
    格式化显示文件的编译
    :%s/ /\r /g
  3. bear make qemu 在xv6内生成 compile_commands.json

调度

二十、处理器调度

  • 最简单最直接的调度方法:轮询调度法或称时间片轮转法。

考虑到上述调度算法的缺陷,引入优先级策略

nice : [-20,19] 数字越大优先级越低。nice好人卡,nice=19老好人了,谁要CPU 就给谁,一点不抢;nice=-20,这个人坏极了,一直霸占cpu。

  • 基于优先级的调度

    • RTOS: 优先级高的抢占优先级低的,优先级高的总是先执行,直到高优先级程序放弃cpu执行权
    • Linux:nice相差10,CPU资源获取率相差10倍
    • nice/renice:
      将某个进程绑定到某个cpu上
      taskset -c 0 nice -n 19 yes > /dev/null &
      taskset -c 0 nice -n 9 yes > /dev/null &
  • 动态优先级(MLFQ)

  • Complete Fair Scheduling (CFS)

    const int sched_prio_to_weight[40] =
    {
    /*-20*/ 88761,71755,56483,46273,36291, 
    /*-15*/ 29154,23254,18705,14949,11916, 
    /*-10*/ 9548,7620,6100,4904,3906, 
    /*-5*/  3121,2501,1991,1586,1277, 
    /*0*/   1024,820,655,526,423, 
    /*5*/   335,272,215,172,137, 
    /*10*/  110, 87, 70, 56,45, 
    /*15*/  36, 29, 23, 18, 15, 
    }
    

    哪个程序占用的 虚拟cpu时间 短就给谁 CPU

没有完美的调度算法,不存在一种包揽所有场景的调度算法。 算法设计需要和业务相配合

Linux Namespace Control Groups(cgroups)

namespace轻量级虚拟化

cgroups 允许以进程组为单位管理资源


man proc

NAME

proc - 进程信息,伪文件系统

描述:

proc伪文件系统

推荐阅读

The GNU C Library

newlibc

相关推荐
写bug如流水1 分钟前
在Ubuntu 20.04上安装pgAdmin 4
linux·运维·ubuntu
冰红茶兑滴水8 分钟前
Linux 线程控制
linux·c++·算法
IT良20 分钟前
while循环及简单案例
linux·服务器
码哝小鱼31 分钟前
iptables限制网速
linux·服务器·网络
leaoleao沄40 分钟前
linux-IO-进程-线程(相关函数)
linux·运维·服务器
frank00600711 小时前
linux 使用mdadm 创建raid0 nvme 磁盘
linux·运维
绿白尼1 小时前
进程与线程
linux
iangyu1 小时前
linux命令之pwdx
linux·运维·服务器
C语言扫地僧1 小时前
Docker 镜像制作(Dockerfile)
linux·服务器·docker·容器
Xinan_____2 小时前
Linux——高流量 高并发(访问场景) 高可用(架构要求)
linux·运维·架构