0%

操作系统and编译随记(4)

在操作系统上实现进程

Thread OS

#include <am.h>
#include <klib.h>
#include <klib-macros.h>

#define NTASK 2

typedef union task {
struct {
union task *next;
void (*entry)(void *);
Context *context;
};
uint8_t stack[8192];
} Task;

Task *current, tasks[NTASK];

void thread_entry(void *arg) {
while (1) {
yield();
}
}

Context *on_interrupt(Event ev, Context *ctx) {
if (!current) {
current = &tasks[0]; // First trap
} else {
current->context = ctx;
current = current->next;
}
return current->context;
}

int main() {
cte_init(on_interrupt);

for (int i = 0; i < LENGTH(tasks); i++) {
Task *task = &tasks[i];
Area stack = (Area) { &task->context + 1, task + 1 };
task->context = kcontext(stack, thread_entry, NULL);
task->next = &tasks[(i + 1) % LENGTH(tasks)];
}
yield();
}

上面的系统与真实的进程有什么区别

  • 每个进程都能看到所有变量
  • 进程的地址空间不是独立的

    如何解决

  • 硬件对内存地址进行翻译,将虚拟内存地址翻译为物理内存地址
  • picture 1
  • 用户态的程序不能够访问上述映射的数据结构
  • 系统虚拟地址空间映射到实际地址空间可能不是全部的(可能没有分配物理页面
    • 访问的没有分配的页面的时候会有缺页异常被调用
    • 只是在地址空间上做了一个标记
  • 使用二叉搜索树找到某一个内存空间的位置(一个有序的集合)

    多个进程的不同虚拟地址映射到同一个位置

  • 比如很多进程都用到libc,但是所有进程都用的是同一个而且只读
  • 证明:找到映射的物理地址是同一个
  • 复制的不同进程中使用malloc分配的是不同的,但是代码部分的地址是相同的,不会真的复制

    fork()写时复制

  • picture 2
  • 防止fork之后直接执行execve浪费时间
  • 只读的部分就不需要拷贝
  • 写的时候触发一个缺页中断,写这个位置的进程在检查权限之后会拷贝一次这个页面
  • 注意是按页面拷贝的,而不是按变量拷贝的,比如在一个巨大的数组中修改一个值,只拷贝被修改的这个页面

    利用fork()创建内存的历史记录

  • 一个长期执行的应用程序防止出现bug导致crash,可以每隔一段时间fork()一次,相当于记录了一个快照,如果出错的话可以回退

    进程调度

  • picture 3

    手动控制CPU的调度

  • Linux没有硬实时的机制,只能体现在CPU资源获得的控制上
  • 使用nice分配CPU

    多级反馈调度队列

  • 如果一个程序是计算密集的,可以往后排,但是如果这个程序需要与用户交互那就需要往前排
  • 实现的方式是如果一个程序没有用完时间片就主动交出CPU,就提高它的优先级,否则降低

    尽量公平分配CPU

  • picture 4
  • 中断的时候补偿获得过CPU最少的进程
  • 使用红黑树把所有可以调度的进程的时间排序

    如何在公平的调度之下实现不同的优先级

  • 不同进程运行的时间的计算方式不同,不同的人的时钟不一样快即可,优先级高的人的时钟慢,否则就快一些

    真实的CPU调度

  • 低优先级的进程持有锁的时候被高优先级打断了怎么办
  • 实时操作系统的操作就是高优先级的进程可以无条件抢占低优先级的进程
  • 操作系统需要知道到底谁获得了什么锁,在什么时候释放
  • 优先级继承:
    • 低优先级的任务阻碍了高优先级的任务,则低优先级的任务会继承高优先级任务的优先级
    • 优先级反转
      • picture 5

        多处理器调度

  • picture 6
  • picture 7
  • 处理器逻辑门跳变的时候会发热产生功耗,也就是所谓的动态功耗(另外还有静态功耗)

    什么是core dump

  • 将程序crash的时候的内存保存成elf文件,事后可以用gdb调试

    输入输出设备

  • I/O是什么
  • 设备抽象成寄存器
    • picture 8
  • 将设备寄存器映射到内粗地址
  • CPU像访问内存一样访问寄存器

    设备树

  • 告诉系统任何设备在什么位置
  • picture 9

    总线

  • 如何在不增加系统IO设计的基础上扩展其他设备
  • 通过总线连接设备
  • 连接其他总线
  • 将所有的IO设备统一管理起来的设备
  • picture 10
  • PCI可以连接USB总线
  • 速度快的设备是直接连接在PCI总线上的
    • 显卡是直接挂载PCI总线上的
    • 千兆网卡
    • 桥接USB、SATA等控制器也可以

      中断

  • 什么是中断?
  • 中断就是CPU上的一根线
  • 处理重要的中断的时候需要屏蔽掉其他的中断

    DMA

  • 大量的内存拷贝需要占用很长的CPU时间
  • picture 12
  • 此时引入一个专门的拷贝控制器
  • picture 11
  • 此时内存复制不再需要CPU的直接参与,节约时间
  • 用于控制内存和设备之间传输数据

    GPU

  • picture 13
  • 将通用计算简化,得到可以大量并行的计算

    如何绘制三维画面

  • 任何一个多边形都可以分为n-2个三角形
  • 为每个三角形贴图
  • 将三角形投射到摄像机的平面上
  • picture 14

    IO设备的抽象和分类

  • picture 15

    分类

  • 字节流,读取字符然后丢弃
  • 块设备,读取字节数组按块访问

    设备驱动是什么

  • picture 16
  • 就是一段内核代码
  • 将系统对设备的readwrite等操作翻译成设备对应的操作
  • 设备驱动程序完成的就是设备的通讯协议
  • /dev下就是设备文件
    • /dev/stdout就是标准输出,还有/dev/stdin, /dev/stderr
    • echo Hello > /dev/tty这句话会把Hello输出到命令行上
    • /dev/tty就是命令行
    • 但是用户实际使用的是虚拟终端,也就是所谓的/dev/pts/数字序号
      • 不同的终端数字不同
    • /dev/zero是一个0设备,不断生成0
    • /dev/null设备是一个NULL设备,将其他设备的输出重定向给这个设备,就会直接被扔掉
      • 这个设备的weite函数什么都不做
    • /dev/random生成随机数,认为是真的随机数
    • /dev/urandom是随机性不太高的随机数
  • picture 17
  • picture 18
    • 将一切额外的复杂性都丢给了控制,也就是ioctl
    • 将所有的复杂性都丢给了文件系统
    • 导致系统存在很多隐形的依赖关系,比如/proc等等

      字符设备

  • 内核模块实例
    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/init.h>
    #include <linux/cdev.h>
    #include <linux/device.h>
    #include <linux/fs.h>
    #include <linux/uaccess.h>

    #define MAX_DEV 2

    static int dev_major = 0;
    static struct class *lx_class = NULL;
    static struct cdev cdev;

    static ssize_t lx_read(struct file *, char __user *, size_t, loff_t *);
    static ssize_t lx_write(struct file *, const char __user *, size_t, loff_t *);

    static struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = lx_read,
    .write = lx_write,
    };

    static struct nuke {
    struct cdev cdev;
    } devs[MAX_DEV];

    static int __init lx_init(void) {
    dev_t dev;
    int i;

    // allocate device range
    alloc_chrdev_region(&dev, 0, 1, "nuke");

    // create device major number
    dev_major = MAJOR(dev);

    // create class
    lx_class = class_create(THIS_MODULE, "nuke");

    for (i = 0; i < MAX_DEV; i++) {
    // register device
    cdev_init(&devs[i].cdev, &fops);
    cdev.owner = THIS_MODULE;
    cdev_add(&devs[i].cdev, MKDEV(dev_major, i), 1);
    device_create(lx_class, NULL, MKDEV(dev_major, i), NULL, "nuke%d", i);
    }
    return 0;
    }

    static void __exit lx_exit(void) {
    device_destroy(lx_class, MKDEV(dev_major, 0));
    class_unregister(lx_class);
    class_destroy(lx_class);
    unregister_chrdev_region(MKDEV(dev_major, 0), MINORMASK);
    }

    static ssize_t lx_read(struct file *file, char __user *buf, size_t count, loff_t *offset) {
    if (*offset != 0) {
    return 0;
    } else {
    uint8_t *data = "This is dangerous!\n";
    size_t datalen = strlen(data);
    if (count > datalen) {
    count = datalen;
    }
    if (copy_to_user(buf, data, count)) {
    return -EFAULT;
    }
    *offset += count;
    return count;
    }
    }

    static ssize_t lx_write(struct file *file, const char __user *buf, size_t count, loff_t *offset) {
    char databuf[4] = "\0\0\0\0";
    if (count > 4) {
    count = 4;
    }

    copy_from_user(databuf, buf, count);
    if (strncmp(databuf, "\x01\x14\x05\x14", 4) == 0) {
    const char *EXPLODE[] = {
    " ⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⣀⣀⠀⠀⣀⣤⣤⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
    " ⠀⠀⠀⣀⣠⣤⣤⣾⣿⣿⣿⣿⣷⣾⣿⣿⣿⣿⣿⣶⣿⣿⣿⣶⣤⡀⠀⠀⠀⠀",
    " ⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⠀⠀⠀⠀",
    " ⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⡀⠀",
    " ⠀⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀",
    " ⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠟⠁⠀",
    " ⠀⠀⠻⢿⡿⢿⣿⣿⣿⣿⠟⠛⠛⠋⣀⣀⠙⠻⠿⠿⠋⠻⢿⣿⣿⠟⠀⠀⠀⠀",
    " ⠀⠀⠀⠀⠀⠀⠈⠉⣉⣠⣴⣷⣶⣿⣿⣿⣿⣶⣶⣶⣾⣶⠀⠀⠀⠀⠀⠀⠀⠀",
    " ⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠋⠈⠛⠿⠟⠉⠻⠿⠋⠉⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀",
    " ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣶⣷⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
    " ⠀⠀⠀⠀⠀⠀⢀⣀⣠⣤⣤⣤⣤⣶⣿⣿⣷⣦⣤⣤⣤⣤⣀⣀⠀⠀⠀⠀⠀⠀",
    " ⠀⠀⠀⠀⢰⣿⠛⠉⠉⠁⠀⠀⠀⢸⣿⣿⣧⠀⠀⠀⠀⠉⠉⠙⢻⣷⠀⠀⠀⠀",
    " ⠀⠀⠀⠀⠀⠙⠻⠷⠶⣶⣤⣤⣤⣿⣿⣿⣿⣦⣤⣤⣴⡶⠶⠟⠛⠁⠀⠀⠀⠀",
    " ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
    " ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠒⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠓⠀⠀⠀⠀⠀⠀⠀⠀⠀",
    };
    int i;

    for (i = 0; i < sizeof(EXPLODE) / sizeof(EXPLODE[0]); i++) {
    printk("\033[01;31m%s\033[0m\n", EXPLODE[i]);
    }
    } else {
    printk("nuke: incorrect secret, cannot lanuch.\n");
    }
    return count;
    }

    module_init(lx_init);
    module_exit(lx_exit);
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("jyy");
  • 更多file operations
    • picture 19

      块设备

  • picture 20
  • 是一种共享的设备
  • 不能直接写入字节,必须先把所在范围内的全部擦除,然后才能写入
  • 一次写入的大小是一个block,不是随机访问的
  • 在文件系统和磁盘设备之间
    • picture 21
    • picture 22
  • 因此通过隔离实现了虚拟化保障安全
    • picture 23
    • 文件系统是磁盘的虚拟化
    • 将一个磁盘变成多个可扩展的动态字节序列(虚拟磁盘)
  • 将虚拟磁盘映射到进程的地址空间
    • picture 24
    • 可以改变虚拟磁盘(文件)的大小
  • fork的时候会继承偏移量
    • picture 25
    • 假如父子进程一起write一个文件100字节,会共享同一个偏移量,互相不覆盖但是顺序会竞争

      文件系统

  • Unix系统的文件系统只有一个根/
  • 外接存储设备
    • 可以将任何一个磁盘上的文件系统挂载到文件系统中的任何一个节点上
    • 可以把各种各样的文件系统挂载到任何一个节点上
    • 目录拼接
    • 磁盘分区也可以通过挂载将其包含到文件系统中
    • mount方法,mount 设备 要挂载到的地址
      • picture 26
  • loop是回环设备,将文件变为磁盘设备
  • picture 29

    切换根文件系统

  • 使用switch_root命令
  • 执行的内容包括
    • 删除根文件系统的全部内容,节省空间
    • 安装新的文件系统并切换到这个文件系统
    • stdin, stdout, stderr附加到新的/dev/console,然后执行新文件系统的init程序
  • 命令格式
  • switch_root [-c /dev/console] NEW_ROOT NEW_INIT [ARGUMENTS_TO_INIT]
    • 其中NEW_ROOT是实际的根文件系统的挂载目录,执行switch_root命令前需要挂载到系统中
    • NEW_INIT是实际根文件系统的init程序的路径,一般是/sbin/init
    • -c /dev/console是可选参数,用于重定向实际的根文件系统的设备文件,一般情况我们不会使用
    • ARGUMENTS_TO_INIT则是传递给实际的根文件系统的init程序的参数,也是可选的

      将一个磁盘映像文件挂载到文件系统之中

  • .img文件
  • mnt xxx.img /路径
  • 系统将这个文件使用一个loop创建为一个设备
  • 然后挂载这个设备
    • picture 27

      Linux文件系统的标准

  • picture 28

    使用python访问目录

    from pathlib import Path

    for f in Path('/proc').glob('*/status'):
    print(f.parts[-2], \
    (f.parent / 'cmdline').read_text() or '[kernel]')

    创建链接

    硬链接

  • ln [参数] [源文件或目录] [目标文件或目录]
  • 创建的是硬链接,实际上指向的是同一个文件,文件的编号都是相同的
  • 修改一个就是修改所有
  • 完全不可区分
  • 便于节省空间
  • 仅仅存储指向实际文件数据的指针
  • 不可链接目录
  • 不可跨文件系统

    软链接

  • 类似于快捷方式
  • 本身就是一个文件
  • 当引用这个文件时,去找另一个文件
  • 另一个文件的绝对/相对路径以文本形式存储在文件里
  • 可以跨文件系统、可以链接目录
  • 链接指向的位置当前不存在也没关系
  • ln -s 目标 快捷方式
  • 允许创建一个到上一级目录的软连接
  • 允许成环
    • 无限递归
    • 使用find寻找的时候,会报错检测到了文件系统回环