系列文章目录
文章目录
前言
十一、操作系统上的进程
操纵系统内核的启动:CPU Reset->Firmware->Boot loader->Kernel_start()->…
init进程, init进程通过系统调用创建Linux中的所有…
- 操作系统启动后做了什么?
- 操作系统如何管理程序(进程)
1. 从系统启动到第一个进程
eg:
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将当前状态复制
/* 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;
操作系统:状态机的管理者
虚拟化:操作系统可以同时管理多个状态机,每一步选择一个状态执行
#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);
}
#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); 不缓冲标准输出
解释:
printf("Hello"); // fflush(stdout);
int *ptr = nullptr;
*ptr = 1;
若无 // 程序报错时不会输出hello,若在输出前清空缓冲区则在报错前会输出hello
printf(); 会缓冲标准输出,fork时缓冲区的内容也会同步复制,因此./a.out | wc -l
为8
execv()
/* 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));
重置状态机,
man execve
bash -c env # 显示当前环境变量
#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环境变量
PATH=x:y:z gcc test.c # 改变gcc环境变量从而到会不能正常运行
销毁进程
销毁状态机
return;
void exit(int status); // 进程normal terminate,将 status 返回给父进程
_exit();
syscall(SYS_exit, 0);
#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);
}
详情:
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
进程地址空间管理
#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)
// 更易用的封装接口
execlp("echo", "hello", "world", nullptr);
环境变量:env, bash -c env
实现:打印环境变量的小程序:
#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下的文件描述符
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效率得到提升
- 操作系统会维护每个页面的引用计数
申请内存,malloc(128M);
for(int i=0; i<1000; ++i) {
if (pid == 0) break;
}
使用内存
- 所以,真个操作系统里libc代码和只读数据只有一个副本!
- 推论:统计进程占用的内存是个伪命题
所以一个进程占用了多少内存如何计算?
用虚拟内存查看内存泄露
other
利用fork实现回溯
dfs_fork
#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:
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
#! 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
int main()
{
hello();
}
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
loader-static.c
#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
#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时:
- make -nB qemu
- 想要知道所有命令调用序列
make -nB qemu | vim -
:set nowrap
格式化显示文件的编译
:%s/ /\r /g - 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
本站资源均来自互联网,仅供研究学习,禁止违法使用和商用,产生法律纠纷本站概不负责!如果侵犯了您的权益请与我们联系!
转载请注明出处: 免费源码网-免费的源码资源网站 » 操作系统概述(三、虚拟化)
发表评论 取消回复