0.11常用命令 linux
2017-07-02 21:24:14 junlon2006 阅读数 197
转载于:http://tinylab.org/take-5-minutes-to-build-linux-0-11-experiment-envrionment/

1 故事

大概在 2008 年 5 月份开始阅读赵博的《Linux内核完全注释》,并在当时 兰大开源社区 的博客系统上连载阅读笔记。

每阅读完一部分就会写一份笔记,当时社区的反响还是蛮大了,因此结识了很多技术方面的好友。

但是大概在 2009 年初,自己出去实习了。因为实习工作任务繁重,所以这部分阅读工作未能继续。另外,那个博客网站因为升级故障,导致数据被破坏,到如今都无法访问。

还好当时有做数据备份,2013 年左右在自己机器上重新把网站恢复出来,博客系统的数据总算找回来。并且已经陆续把部分重要文章整理到了如今的 泰晓科技 平台上,希望更多的同学受益。

计划逐步把当时的阅读笔记整理出来并抽空阅读剩下的部分。

这里先分享如何快速搭建一个 Linux 0.11 的实验环境,这是阅读这本书非常重要的准备工作,因为作为实践性很强的操作系统课程,实验环境是必要条件,否则无法深入理解。

2 更多细节

好了,如果想快速上手,可以直接跳到 下一节

2.1 往事回首

赵老师书里头介绍的是在 Redhat 环境下用 Bochs 仿真系统来跑 Linux 0.11,通过实验发现诸多问题,不断摸索,阅读计划不断推迟,因为蛮多时间浪费在实验和调试环境的打造上了,分享一下这段历史吧:目前已经成功地把linux-0.11移植到gcc 4.3.2,当时还是做了一些工作的:

  • 可以在 32 位和 64 位的 Linux/x86 系统上编译
  • 支持最新的 Gcc 4.3.2,并同时支持 Gcc 3.4, 4.1, 4.2, 4.3,也就是说不管你机器上安装的是这里头的哪个版本,该代码都可以正常编译
  • 在最新的 Ubuntu 和 Debian 系统上测试通过
  • 在 bochs 2.3.7 和 qemu 0.9.1 上正常启动
  • 其中的 boot/bootsect.s 和 boot/setup.s 已经用 AT&T 语法重写,并把原来的版本剔除,因此无须再安装 bin86
  • Makefile 文件被调整和增加了一些内容,更方便用户调整编译选项和移植,并更方便地进行实验
  • 用 Shell 重写了 tools/build.c,更容易理解

最终达成的效果是,可以非常方便地在当时最新的 Ubuntu 系统上学习和调试 Linux 0.11,为后续进一步研究 Linux 0.11 提供了最基础的准备。

2.2 八年之后

废话不多说了,从 2008 年到现在,自己在 Linux 方面的学习有了一定的进步,回头再看看曾经奋斗的历程,稍微有点小小的感动。

因为当时的博客以及档案的下载地址都已经失效,所以很多网友还时长会发邮件过来咨询。一般是直接把机器上备份的一些档案邮寄给大家。

最近也稍有在 Google 上检索 Linux 0.11,非常有幸看到有蛮多的 github 仓库备份并改进了我当时上传的代码。非常精彩的例子有:

  • https://github.com/yuanxinyu/Linux-0.11
  • https://github.com/run/linux0.11

非常感谢大家的工作,上面都可以直接在当前的 Ubuntu 环境下工作,第一份甚至都已经支持 Mac OS X ;-)

不过也还有可稍微改进的地方:

  • 把实验需要的 rootfs 镜像直接通过压缩的方式上传到仓库

    这样就可以形成一个完整的实验环境,压缩的好处是可以加快网络下载的速度;另外,为了避免额外的解压工作,在 Makefile 里头,我这个脚本控当然是代劳了。

  • 合并更多未曾发布的内容

    • 把 calltree 二进制文件直接打包进去,这样就可以直接用了(注:calltree-2.3 源代码也已经无法在最新的 Ubuntu 系统编译了!)
    • 添加了脚本 tools/tree2dotx,可以把 calltree 的输出转换为图片
    • 把 floppy 和 ramdisk 的包也打包进去,方便阅读相关代码,不过可惜的是,发现从 floppy 启动一直死循环,后面再解决吧,应该是代码问题
    • 其他微小调整

整个实验环境目前只有 30 M,压缩成 xz 只有 18 M,非常小。

3 五分钟教程

3.1 预备

先准备个电脑,XP 已死,建议用 Ubuntu 或者 OS X,这里主要介绍 Ubuntu,OS X 看 README.md 吧。

  1. apt-get install vim cscope exuberant-ctags gcc gdb binutils qemu

3.2 下载

  1. git clone https://github.com/tinyclub/linux-0.11-lab.git

3.3 编译

  1. cd linux-0.11-lab && make

3.4 从硬盘启动

  1. make start-hd

3.5 调试

打开一个控制台,从硬盘启动并进入 debug 模式:

  1. make debug-hd

通过 gdb 调试:

  1. gdb images/kernel.sym
  2. (gdb) target remote :1234
  3. (gdb) b main
  4. (gdb) c

3.6 查阅文档

README.md

3.7 查看帮助

  1. make help
  2. > Usage:
  3. make --generate a kernel floppy Image with a fs on hda1
  4. make start -- boot the kernel in qemu
  5. make start-fd -- boot the kernel with fs in floppy
  6. make start-hd -- boot the kernel with fs in hard disk
  7. make debug -- debug the kernel in qemu & gdb at port 1234
  8. make debug-fd -- debug the kernel with fs in floppy
  9. make debug-hd -- debug the kernel with fs in hard disk
  10. make disk -- generate a kernel Image & copy it to floppy
  11. make cscope -- genereate the cscope index databases
  12. make tags -- generate the tag file
  13. make cg -- generate callgraph of the system architecture
  14. make clean -- clean the object files
  15. make distclean -- only keep the source code files

3.8 生成函数调用关系图

  1. make cg
  2. ls calltree/linux-0.11.jpg

生成的图片见最后。


4 后话

是不是够简单便捷?

遇到任何问题欢迎参与回复互动,或者关注我们的新浪微博/微信公众号互动:@泰晓科技。

也可以直接到赵老师的站点上参与交流和讨论:http://www.oldlinux.org/oldlinux/


2018-08-18 14:46:10 yangzhengqui 阅读数 73

当调用fork的时候,因为是系统调用,所以会调用_syscall0这个函数

#define _syscall0(type,name) \
  type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
    : "=a" (__res) \
    : "0" (__NR_##name)); \
if (__res >= 0) \
    return (type) __res; \
errno = -__res; \
return -1; \
}

这里是一段汇编代码,其中 int $0x80 是通过系统软中断陷入到内核,然后跳转到system_call里面执行。system_call是一段汇编代码:

system_call:
    cmpl $nr_system_calls-1,%eax
    ja bad_sys_call
    push %ds
    push %es
    push %fs
    pushl %edx
    pushl %ecx      # push %ebx,%ecx,%edx as parameters
    pushl %ebx      # to the system call
    movl $0x10,%edx        # set up ds,es to kernel space
    mov %dx,%ds
    mov %dx,%es
    movl $0x17,%edx        # fs points to local data space
    mov %dx,%fs
    call *sys_call_table(,%eax,4)
    pushl %eax
    movl current,%eax
    cmpl $0,state(%eax)        # state
    jne reschedule
    cmpl $0,counter(%eax)      # counter
    je reschedule

这段汇编代码的意思是_syscall0 调用的时候把参数压入堆栈,然后在sys_call_table找到对应的函数,然后调用对应的函数.这里是调用了对应的sys_fork.

.align 2
sys_fork:
    call find_empty_process
    testl %eax,%eax
    js 1f
    push %gs
    pushl %esi
    pushl %edi
    pushl %ebp
    pushl %eax
    call copy_process
    addl $20,%esp
1:  ret

在sys_fork里面先调用了find_empty_process,
find_empty_process的目的是找到最大的不重复的进程PID,用在copy_process用,
实现是使用一个全局变量last_pid保存最大的PID,当需要产生一个进程的时候,先遍历进程链表
task,找出最大的PID,然后跳转到repeat,进行+1操作,此时lastID就是最大的了。

int find_empty_process(void)
{
    int i;

    repeat:
        if ((++last_pid)<0) last_pid=1;
        for(i=0 ; i<NR_TASKS ; i++)
            if (task[i] && task[i]->pid == last_pid) goto repeat;
    for(i=1 ; i<NR_TASKS ; i++)
        if (!task[i])
            return i;
    return -EAGAIN;
}

接下来把参数都压入堆栈,然后调用copy_process这个函数,具体的实现:

int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
        long ebx,long ecx,long edx,
        long fs,long es,long ds,
        long eip,long cs,long eflags,long esp,long ss)
{
//进程结构体
    struct task_struct *p;
    int i;
    //文件结构体
    struct file *f;
    //获得空闲的页表,一页为4k
    p = (struct task_struct *) get_free_page();
    if (!p)
        return -EAGAIN;
    //nr为find_empty_process返回来的可用的任务号,把新的进程结构赋值给新的任务
    task[nr] = p;

    // NOTE!: the following statement now work with gcc 4.3.2 now, and you
    // must compile _THIS_ memcpy without no -O of gcc.#ifndef GCC4_3
    *p = *current;  /* NOTE! this doesn't copy the supervisor stack */
    //先置为不可中断休眠,避免不小心切换进来
    p->state = TASK_UNINTERRUPTIBLE; 
    //新子进程的pid 
    p->pid = last_pid;
    //父进程
    p->father = current->pid;
    //时间片为新进程的优先级
    p->counter = p->priority;
    //清空信号
    p->signal = 0;
    //清空报警
    p->alarm = 0;
    //进程的领导不能被继承
    p->leader = 0;      /* process leadership doesn't inherit */
    //清空统计时间
    p->utime = p->stime = 0;
    //新进程内核态和用户态时间清零
    p->cutime = p->cstime = 0;
    //新进程的开始时间
    p->start_time = jiffies;
    p->tss.back_link = 0;
    p->tss.esp0 = PAGE_SIZE + (long) p;
    //设置寄存器的内容
    p->tss.ss0 = 0x10;
    p->tss.eip = eip;
    p->tss.eflags = eflags;
    p->tss.eax = 0;
    p->tss.ecx = ecx;
    p->tss.edx = edx;
    p->tss.ebx = ebx;
    p->tss.esp = esp;
    p->tss.ebp = ebp;
    p->tss.esi = esi;
    p->tss.edi = edi;
    p->tss.es = es & 0xffff;
    p->tss.cs = cs & 0xffff;
    p->tss.ss = ss & 0xffff;
    p->tss.ds = ds & 0xffff;
    p->tss.fs = fs & 0xffff;
    p->tss.gs = gs & 0xffff;
    p->tss.ldt = _LDT(nr);
    p->tss.trace_bitmap = 0x80000000;
    if (last_task_used_math == current)
        __asm__("clts ; fnsave %0"::"m" (p->tss.i387));
    //调用copy_mem()函数根据父进程的代码端和数据段的地址加上偏移量(nr * 0x4000000)重            //新设置子进程代码段和数据段,并复制页表
    if (copy_mem(nr,p)) {
        task[nr] = NULL;
        free_page((long) p);
        return -EAGAIN;
    }
    //假如文件是打开的,那该文件的系统文件打开表增一,父子进程能共享这些文件打开表,另外pwd等等这些引用都要增一
    for (i=0; i<NR_OPEN;i++)
        if ((f=p->filp[i]))
            f->f_count++;
    if (current->pwd)
        current->pwd->i_count++;
    if (current->root)
        current->root->i_count++;
    if (current->executable)
        current->executable->i_count++;

    /*gdt的前四个分别是null,内核代码段,内核数据段,系统调用段,加上FIRST_TSS_ENTRY 就是跳过前面四个段,接下来就是任务0TSS段和LDT段,任务1TSSLDT,以此类推,所以加上(nr<<1)就是指向了第nr个任务在GDT中的内容,第一句话设置了该任务的tss在GDT中的描述符,第二句设置了ldt在GDT的描述符,之后使任务变为可运行,然后退出*/   
    set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
    set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
    p->state = TASK_RUNNING;    /* do this last, just in case */
    //返回进程ID
    return last_pid;
}
2011-10-16 22:02:56 Sencha_Android 阅读数 2006

其实一直以来对/dev/tty* ./dev/tty,之类的文件就很有点疑惑。今天阅读了linux0.11相关的源码,了解了一下。

在linux0.11内核中字符设备只有三个(两种),一个是控制台设备(我理解就是本机的显示屏和键盘),另外两个是串行端口的串行终端设备。在内核中三个设备用一个tty_struct数组存储。tty_struct中我觉得离应用比较近的就是tty读队列和tty写队列两个主要成员。应用程序从指定的设备文件读取数据或是写入时,在底层实际都是通过对这两个对立操作完成.

我们在编程中使用open()打开指定设备文件,根据设备文件的类型选择读写函数。调用顺序是调用rw_char(READ,dev,buf,count,pos)。根据设备的主设备号,调用char_dev.c中的函数,linux0.11只提供了主设备号为4和5两个读写函数,分别是rw_tty(),rw_ttyx(),书上注释rw_ttyx是串口终端读写函数,rw_tty()是终端读写操作函数。其实实现上并没有太大不同,只是在函数rw_tty中会判断当前进程是否有对应的控制终端,,若有的话则调用rw_ttyx(),但是rw_ttyx中的参数Minor是从current->tty得来。在rw_ttyx函数中根据度读写动作调用tty_io.c 中的对应的读写函数tty_read 和tty_write.在tty_read和tty_write中就是根据minor参数执行的索引在上面说的tty_struct数组中需找tty_struct,读取tty读队列,写入tty写队列。

举例来说读取/dev/tty(在linux0.11下是一个主设备号为5,此设备号为0的字符设备),因为主设备号5,所以调用的是rw_tty,在rw_tty中检查当前进程的tty是否存在(current->tty>=0),若存在则使用该tty作为子设备号,调用函数读取控制台设备,两个串行端口的串行端口设备中的一个的读队列。调用结束。

写的比较乱,总结一下,如果主设备号为5则会根据次设备号读取或写入设备数组中的设备的队列,如果主设备号为4则会根据当前进程的tty作为次设备号操作设备数组中相应设备的队列。

ps:较高版本的内核的tty有鉴于本人水平不够,就不分析了,后面如果知道了会补上。Mark安静


2009-03-26 18:29:00 boyskung 阅读数 577

注意,下文提到的任务和进程是指同一个概念

1、 相关数据结构和寄存器

 

a、  任务控制块:

struct task_struct {

/* these are hardcoded - don't touch */

       long state;       /* -1 unrunnable, 0 runnable, >0 stopped */

       long counter;

       long priority;

       long signal;

       struct sigaction sigaction[32];

       long blocked;  /* bitmap of masked signals */

/* various fields */

       int exit_code;

       unsigned long start_code,end_code,end_data,brk,start_stack; //任务在内存中分布相关数据

       long pid,father,pgrp,session,leader;

       unsigned short uid,euid,suid;

       unsigned short gid,egid,sgid;

       long alarm;

       long utime,stime,cutime,cstime,start_time;

       unsigned short used_math;

/* file system info */

       int tty;            /* -1 if no tty, so it must be signed */

       unsigned short umask;

       struct m_inode * pwd;

       struct m_inode * root;

       struct m_inode * executable;//任务的可执行文件i节点

       unsigned long close_on_exec;

       struct file * filp[NR_OPEN];

/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */

       struct desc_struct ldt[3];

/* tss for this task */

       struct tss_struct tss;

};

此结构保存了当前任务的相关数据,用于操作系统对其进行管理。

 

b、  任务状态段

struct tss_struct {

       long back_link;       /* 16 high bits zero */

       long esp0;

       long ss0;        /* 16 high bits zero */

       long esp1;

       long ss1;        /* 16 high bits zero */

       long esp2;

       long ss2;        /* 16 high bits zero */

       long cr3;//页目录基地址寄存器

       long eip;

       long eflags;

       long eax,ecx,edx,ebx;

       long esp;

       long ebp;

       long esi;

       long edi;

       long es;          /* 16 high bits zero */

       long cs;          /* 16 high bits zero */

       long ss;          /* 16 high bits zero */

       long ds;          /* 16 high bits zero */

       long fs;          /* 16 high bits zero */

       long gs;          /* 16 high bits zero */

       long ldt;          /* 16 high bits zero *///任务的ldt段选择符。

       long trace_bitmap;  /* bits: trace 0, bitmap 16-31 */

       struct i387_struct i387;

};

此结构保存了任务的处理器状态,指定了构成任务执行空间的各个段

 

c、  任务结构指针数组

struct task_struct * task[NR_TASKS] = {&(init_task.task), };

一共可以管理64个任务(NR_TASKS = 64

 

d、  任务寄存器

TR(任务寄存器)中存放着16位的TSS段选择符及当前任务TSS段的整个描述符(TR的不可见部分)。这些信息是从GDT中当前任务的TSS描述符中复制过来的。在系统初始化期间加载一次TR寄存器的初值(如,任务0TSS段选择符),随后在系统运行期间,TR内容会在任务切换时自动地被改变。

 

2、 任务状态

 

TASK_RUNNING:任务正在运行或已准备就绪

TASK_INTERRUPTIBLE:任务处于可中断等待状态

TASK_UNINTERRUPTIBLE:任务处于不可中断等待状态

TASK_ZOMBIE:任务处于僵死状态,已经停止运行,但父进程还没发信号

TASK_STOPPED:进程已停止

 

3、 任务的创建

 

当前任务通过fork()系统调用,首先在任务数组task[NR_TASKS]中寻找一个空项,并取得进程号(任务号,即在任务数组中的位置0 <= n <= NR_TASKS),然后复制当前的任务控制块到新的任务控制块中,并对新任务控制块中的内容进行一些修改,包括根据任务新任务号计算新任务的ldt选择符(new->tss.ldt),把运行时间片数设置为其优先级等。然后根据新任务的任务号、task.tss的地址和task.ldt的地址在GDT中设置新任务TSS段和LDT段描述符项,并置新任务为就绪态。

 

当新的任务创建成功后,便可通过execve系统调用加载新的可执行文件执行。调用execve时并没有真正把可执行文件加载到内存中,只是把可执行文件头部信息读到高速缓冲块中,然后修改当前任务的任务控制块中相关数据。接着把调用execve系统调用进程在堆栈上的中断返回地址修改为新的可执行文件的执行入口地址。等中断返回执行新的可执行文件指令时,系统会产生缺页异常,于是异常服务程序便会从硬盘上加载该指令所在页到内存页中,并进行物理地址到虚拟地址的映射,在页目录和页表中添加相应的项。这样做可避免在执行新的可执行文件前大量的硬盘I/O操作,提高了创建进程的效率。

 

4、 退出任务

 

通过调用系统调用exit可退出本进程,也可通过向其进程发送信号SIGKILL,不过这使得程序没有任何机会进行清理工作。在调用exit退出进程时,会释放其所占用的一切资源,并通知其父进程,然后执行任务调度,运行其他进程。

 

5、 任务调度

 

调度时,首先唤醒获得信号的处于可中断等待状态的任务,然后在任务数组task[NR_TASKS]中查找处于就绪态的任务(任务号),在所有就绪太任务中所剩时间片最多的任务将被切换运行。如果有处于运行态的任务,但所有任务的时间片都用完了,则重新每一个任务设置时间片,并重新寻找任务用于切换。如果是没有任务处于运行态,则切换到任务0运行。

 

注意,任务0是个空闲(idle)任务,只有当没有其他任务可以运行时才调用它。它不能被杀死,也不能睡眼。

 

切换任务时,首先检查新任务是不是当前任务,若是则什么也不做就退出。若不是则根据任务号计算出在GDT中新任务的TSS段描述符的选择符(16位,偏移量),然后把当前任务设置为新任务,并通过ljmp指令,跳转到新任务的TSS段选择符,从而造成任务切换到该TSS对应的进程中。

 

6、总结

任务的管理主要依赖于任务控制块(任务描述符)和任务状态段这两个数据结构,其中任务控制块主要用于系统对任务的操作,而任务状态段主要用于任务切换时CPU进行状态保存。

测试

博文 来自: lkun2002

linux 0.11内存管理

阅读数 165

Linux 0.11总结

阅读数 1748

没有更多推荐了,返回首页