2021年12月18日Linux 学习

203 阅读3分钟

linux 大概架构:

linux 架构.jpeg

linux 内核主要两部分:

机制:提供什么功能 策略:何时使用这些功能。

隔离:

WechatIMG153.jpeg

详细的架构图:

WechatIMG154.jpeg

第一个helloword例程: Makefile: 在Makefile 中如果是一条命令就需要用Tab 的方式,这是规矩。

CUREENT_PATH := $(shell pwd)#current path 
KERNEL_VER := $(shell uname -r)# linux kernel code version
KERNELDIR ?= /lib/modules/$(KERNEL_VER)/build


all:
	make -C $(KERNELDIR)  M=$(CUREENT_PATH) modules
clean:
	make -C $(KERNELDIR) M=$(CUREENT_PATH) clean

c文件:这里面的打印使用的是printk ,k代表的是kernel 的意思:

#include <linux/init.h>
#include <linux/module.h>


/**
 * module init fuction lipk_init
 * */
static int __init  lpk_init(void){

    printk(">>>>lpk init \n");
    return 0;
}

static void __exit lpk_exit(void){
    printk(">>>>lpk exit \n");
}

module_init(lpk_init);
module_exit(lpk_exit);

MODULE_LICENSE("GPL");

可以通过lnsmod 来进行加载模块 rmmod 来卸载模块 lsmod 查看是否加载成功。 通过dmesg 来查看日志。

通过下面命令来clone kernel 的代码: git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git

内存寻址:

控制寄存器0-31 位置: 0 --->PE位(protedted enable) 是否进入保护模式 31 --->PG位(page enable) 是否允许分页

PGPE方式
00实模式8080操作
01保护模式,不允许分页
10出错
11允许分页的保护模式

编译:gcc -S helloword.c -o helloword.s 生成汇编文件

汇编:gcc -c helloword.s -o helloword.o

链接:gcc helloword.c -o a.out

装载并执行:./a.out

反汇编:objdump -d a.out

反汇编的代码: 最左边的是虚拟地址, 中间的是指令码, 右边的是AT&T格式的汇编指令

    1149:	f3 0f 1e fa          	endbr64 
    114d:	55                   	push   %rbp
    114e:	48 89 e5             	mov    %rsp,%rbp
    1151:	48 8d 3d ac 0e 00 00 	lea    0xeac(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    1158:	e8 f3 fe ff ff       	callq  1050 <puts@plt>
    115d:	b8 00 00 00 00       	mov    $0x0,%eax
    1162:	5d                   	pop    %rbp
    1163:	c3                   	retq   
    1164:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
    116b:	00 00 00 
    116e:	66 90                	xchg   %ax,%ax

通过下图可以看到,cpu 把虚拟地址送给MMU,MMU是硬件处理单元,然后通过下面分段机制转化为线性地址,然后再通过分也机制转化为物理地址,然后MMU再把这个地址送给存储单元的过程。

WechatIMG155.jpeg

保护模式存在的意义:ring0 ring1 ring2 ring3 内核就是在ring0中,决定着最高特权级,ring3代表着最低特权级(应用层) 这里高特权集可以访问低特权集,反之不能。

描述符表:

全局描述符表GDT(Global descriptor table) 中断描述符表IDT(Interrupt descriptor table) 局部描述符表LDT(Local descriptor table)

Linux段:

线性地址=段的起始地址+偏移量

Linux分页机制

页的大小,在32位的操作系统中,一般默认为4K 大小,页可以2M 4M大小。 在64位的操作系统中,一般默认为4K 8K 最大可以达到256M。 分页的目的:使得每个进程都有自己独立的虚拟内存空间。 映射函数:pa=f(va) 页表存放的是一种虚拟地址和物理地址的映射关系。

0x1000 是4k
0x100000 是1M

虚拟地址->物理地址

MMU虚拟地址(逻辑地址)首先经过分段转化为线性地址,这里有两个部分:1.选择符;2,偏移量。 这里其实就是将进程中的代码和数据段分配在不同的虚拟地址段,避免进程间的互相影响。首先在段选择符中在段描述表中,找到段描述符,也就是某一个段的基地址,再加上段内偏移量,就是得到了对应的线性地址。 再次经过分页的形式将线性地址转化为物理地址。

页分为4级页表:
PGD(page Global dictionary)
PUD (page Upper dictionary)
PMD(page Middle dictionary)
PT( page table)
这四个不一定全用,在有的操作系统中,有可能将中间两个页表置为0. 仍然保留。

CR3 寄存器是保存页全局目录的地址。

进程:

程序一旦运行就形成了进程。 一般通过fork 命令来创建进程。 所有的进程的祖先无一例外时init进程。也称之为0号进程。

打印所有进程:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux>


static void printk_pid(void){
    struct task_struct *task,*p;
    struct list_head *pos;
    int count = 0;
    printk("hello world enter begin:\n");
    task = &init_task;
    list_for_each(pos,&task->tasks){
        p = list_entry(pos,struct task_struct,tasks);
        count ++;
        printk("%d---->%s\n",p->pid,p->comm);
    
    }
    printk("the number of process is:%d\n",count);

}


static int __init print_pid_init(void){
    printk(">>>>>print_pid_init \n");
    printk_pid();
    return 0;
}

static void __exit print_pid_exit(void){
    printk(">>>>>print_pid_init \n");
}





module_init(print_pid_init);
module_exit(print_pid_exit);
MODULE_LICENSE("GPL");

进程时系统资源分配的基本单位,线程时独立运行的基本单位
进程/线程/内核线程 在内核中都是通过do_fork 进行创建。

内核中通过: kthread_create() 来创建线程。最终调用到do_fork 来做事情。
do_fork 位于fork.c 中:

/*
 *  Ok, this is the main fork-routine.
 *
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 */
long do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)

do_fork 所做的事情:
1.copy_process 复制父进程的进程控制块
2.pid = get_task_pid(p, PIDTYPE_PID); 获得子进程的pid
3.wake_up_new_task(p);设置子进程的状态为就绪,并且将子进程加入到就绪队列。

fork完成以后是和父进程一摸一样的进程,只有执行exec 后才是一个新的进程,创建的进程完成所有的代码,会退出,活着在main中return 也可以退出。或者被其他的杀死。也可以退出。

参考资料: www.kerneltravel.net/