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

系列文章目录


文章目录


前言

十一、操作系统上的进程

操纵系统内核的启动: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

相关推荐
朱小弟cs65 小时前
Orange的运维学习日记--41.Ansible基础入门
linux·运维·学习·ci/cd·自动化·ansible·devops
CIb0la6 小时前
kali linux 2025.2安装WPS并设置无报错的详细步骤
linux·运维·wps
醉方休7 小时前
Node.js 精选:50 款文件处理与开发环境工具库
linux·运维·node.js
代码老y8 小时前
从裸机到云原生:Linux 操作系统实战进阶的“四维跃迁”
linux·运维·云原生
CMCST8 小时前
CentOS 7.9 升级 GLibc 2.34
linux·运维·centos
xiep14383335109 小时前
Rocky Linux 10 部署 Kafka 集群
linux·运维·kafka
笨鸟要努力12 小时前
Ubuntu 全盘备份
linux·运维·ubuntu
ChironW12 小时前
Ubuntu 22.04 离线环境下完整安装 Anaconda、CUDA 12.1、NVIDIA 驱动及 cuDNN 8.9.3 教程
linux·运维·人工智能·深度学习·yolo·ubuntu
轻松Ai享生活13 小时前
linux 日志详解
linux
小白的代码日记14 小时前
Linux常用指令
linux·运维·服务器