操作系统真象还原

article/2025/9/16 17:51:05

整体框架
在这里插入图片描述

:mbr.S->loader.S:当os镜像被加载之后,cpu转去main的起止位置开始执行。(到目前为止,控制权已经掌握在自己手里)

接下来就要一步一步搭建起内核模块:


一:实现基本的输入输出功能(lib/kernel/print.h 以及print.S)
接口:put_char() ; put_str() ; put_int() ; set_cursor() ; cls_screen()
实现的语言:汇编。
实现的原理:操纵显存以及显卡端口即可。需要储备显卡相关知识。

二:构建中断框架:操作端口的库的实现(lib/kernel/io.h 用汇编实现)中断实现(kernel/interrupt.h interrupt.c kernel.S)
理论
在这里插入图片描述

搭建流程:
在这里插入图片描述

数据结构:

//中断门描述符struct gate_desc {uint16_t    func_offset_low_word;uint16_t    selector;uint8_t     dcount;   uint8_t     attribute;uint16_t    func_offset_high_word;
};struct gate_desc idt[IDT_DESC_CNT]//idt中断描述符表,在中断初始化的手加载的就是这个
char* intr_name[IDT_DESC_CNT]//保存异常的名字
intr_handler idt_table[IDT_DESC_CNT]//真正的中断处理函数,在kernel.S中调用,kernel.S中是中断入口
intr_handler intr_entry_table[IDT_DESC_CNT]//中断处理函数入口(地址)数组

接口:

enum intr_status {               // 中断状态
INTR_OFF,                    // 中断关闭
INTR_ON                      // 中断打开
};enum intr_status intr_get_status(void);//获得中断状态并
enum intr_status intr_set_status (enum intr_status);//设置新状态并且返回旧状态
enum intr_status intr_enable (void);//打开中断
enum intr_status intr_disable (void);//关闭中断
void register_handler(uint8_t vector_no, intr_handler function);//中断处理函数的注册

中断模块的初始化:

三:构建内存管理模块:字符串库的实现(lib/string.c string.h) 位图库的实现(lib/kernel/bitmap.c bitmap.h)(kernel/memory.c memory.h)
搭建流程
在这里插入图片描述

理论知识:
在这里插入图片描述

1、物理内存的管理:内核物理内存管理 ,用户物理内存管理
两者采用相同的管理方式:
(1)分配内存大小:4KB 以及小于 1024bytes
(2)管理方法:位图(bitmap)

2、虚拟内存管理:内核线程虚拟地址管理,用户态进程虚拟地址管理
(虚拟内存并不需要分配实际的空间,只需要管理每一个进程虚拟空间即可,动态与物理内存管理交互)
管理方法:位图(bitmap)

数据结构:

**内核中只需要持有两个内存池,一个是给内核分配物理内存的,一个是给用户分配内存的**
struct pool {
struct bitmap pool_bitmap;    // 本内存池用到的位图结构,用于管理物理	内存uint32_t phy_addr_start;      // 本内存池所管理物理内存的起始地址uint32_t pool_size;           // 本内存池字节容量
struct lock lock;             // 申请内存时互斥
};**此结构位于每一个页的开头**
struct arena {struct mem_block_desc* desc;  // 此arena关联的mem_block_desc
/* large为ture时,cnt表示的是页框数。* 否则cnt表示空闲mem_block数量 */uint32_t cnt;bool large;
};/* 用于虚拟地址管理 */
**用于管理内核虚拟地址以及为每一个用户进程创建一个该结构来管理用户虚拟地址空间**
struct virtual_addr {
/* 虚拟地址用到的位图结构,用于记录哪些虚拟地址被占用了。以页为单	位。*/struct bitmap vaddr_bitmap;
/* 管理的虚拟地址 */uint32_t vaddr_start;
};/* 内存块 */
struct mem_block {struct list_elem free_elem;
};/* 内存块描述符 */
struct mem_block_desc {uint32_t block_size;          // 内存块大小uint32_t blocks_per_arena;    // 本arena中可容纳此mem_block的数量.struct list free_list;        // 目前可用的mem_block链表
};struct mem_block_desc k_block_descs[DESC_CNT];  // 内核内存块描述符数组
struct pool kernel_pool, user_pool;      // 生成内核内存池和用户内存池
struct virtual_addr kernel_vaddr;        // 此结构是用来给内核分配虚拟地址

有关小于1024bytes字节空间分配数据结构的关系示意图:
在这里插入图片描述

两个内存池:
在这里插入图片描述

接口:

void mem_init(void);//内存管理初始化
void malloc_init(void);
void block_desc_init(struct mem_block_desc* desc_array);void* get_kernel_pages(uint32_t pg_cnt);//从内核池中获取一个物理页,并返回虚拟地址
void* get_user_pages(uint32_t pg_cnt);//从用户池中获得一个物理页,并返回虚拟地址
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt);//从用户池或者内核池中获取一个物理页,并返回虚拟地址。uint32_t* pte_ptr(uint32_t vaddr);//获取pte的物理地址
uint32_t* pde_ptr(uint32_t vaddr);//获取pde的物理地址uint32_t addr_v2p(uint32_t vaddr);//从虚拟地址转换到物理地址void* get_a_page(enum pool_flags pf, uint32_t vaddr);从pf中获得一个物理页并映射到vaddrvoid* sys_malloc(uint32_t size);
void mfree_page(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt);
void pfree(uint32_t pg_phy_addr);
void sys_free(void* ptr);
void* get_a_page_without_opvaddrbitmap(enum pool_flags pf, uint32_t vaddr);
void free_a_phy_page(uint32_t pg_phy_addr);

内存管理模块初始化:
在这里插入图片描述

四:线程模块的实现:链表库的实现(lib/kernel/list.c list.h) 线程库的实现(thread/thread.c thread.h switch.S)
理论:线程或者进程的调度算法:
在这里插入图片描述

搭建过程:
在这里插入图片描述

理论知识:
数据结构:

内核空间维持的两个进程/线程队列:
struct list thread_all_list;
struct list thread_ready_listpid号分配池中断栈intr_stack
struct intr_stack {uint32_t vec_no;     // kernel.S 宏VECTOR中push %1压入的中断号uint32_t edi;uint32_t esi;uint32_t ebp;uint32_t esp_dummy;  // 虽然pushad把esp也压入,但esp是不断变化的,所以会被popad忽略uint32_t ebx;uint32_t edx;uint32_t ecx;uint32_t eax;uint32_t gs;uint32_t fs;uint32_t es;uint32_t ds;/* 以下由cpu从低特权级进入高特权级时压入 */uint32_t err_code;           // err_code会被压入在eip之后void (*eip) (void);uint32_t cs;uint32_t eflags;void* esp;uint32_t ss;
};线程栈thread_stack:struct thread_stack {uint32_t ebp;uint32_t ebx;uint32_t edi;uint32_t esi;/* 线程第一次执行时,eip指向待调用的函数kernel_thread 
其它时候,eip是指向switch_to的返回地址*/void (*eip) (thread_func* func, void* func_arg);/*****   以下仅供第一次被调度上cpu时使用   ****//* 参数unused_ret只为占位置充数为返回地址 */void (*unused_retaddr);thread_func* function;   // 由Kernel_thread所调用的函数名void* func_arg;    // 由Kernel_thread所调用的函数所需的参数
};

接口:

	/* 初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中相应的位置 */void thread_create(struct task_struct* pthread, thread_func function, 					void* func_arg);//伪造好线程环境:thread_stack:其中包括待执行函数的指针,但调度器调度切换到该进程的时候便可以执行该函数,即伪造的断点。struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg);//初始化线程相关信息void init_thread(struct task_struct* pthread, char* name, int prio);

以上两个函数完成了创造线程的工作:内核布局如下图所示:
在这里插入图片描述

	//获取当前进程的task_struct结构struct task_struct* running_thread(void);//任务调度器void schedule(void);//线程模块初始化void thread_init(void);//阻塞进程void thread_block(enum task_status stat);//唤醒进程void thread_unblock(struct task_struct* pthread);//出让调度器但不阻塞当前进程,只是让出处理机,进入就绪队列,等待下次调度void thread_yield(void);pid_t fork_pid(void);void sys_ps(void);//线程退出,回收相关资源void thread_exit(struct task_struct* thread_over, bool need_schedule);struct task_struct* pid2thread(int32_t pid);void release_pid(pid_t pid);

初始化:
在这里插入图片描述

五:同步模块以及键盘等外设驱动模块的实现:(thread/sync.c sync,h)(device/*)
理论
在这里插入图片描述

个人理解:在实现多线程,多进程的系统中,一个重要的模块或者说功能当然是同步与互斥。不管是单核还是多核,再面对共享资源的情况下,可能需要同步操作或者互斥访问。这就需要有信号量和锁这样的机制(注意不要把信号量和锁两个概念相混淆。锁可以通过信号量来实现,也可以用其它方式,如硬件提供的锁。再用信号量实现锁的情况下,锁相当于信号量的值为1的情况。而信号量同时也用于实现同步问题。当用于同步问题的时候,值的大小往往代表了资源的数量。)
我们通过运用开关中断的方式来实现不同线程或者进程之间的同步和互斥问题。

数据结构:

/* 信号量结构 */
struct semaphore {uint8_t  value;struct   list waiters;
};/* 锁结构 */
struct lock {struct   task_struct* holder;            // 锁的持有者struct   semaphore semaphore;            // 用二元信号量实现锁uint32_t holder_repeat_nr;               // 锁的持有者重复申请锁的次数
};

接口:

//信号量初始化
void sema_init(struct semaphore* psema, uint8_t value);
//信号量的p操作
void sema_down(struct semaphore* psema);
//信号量的v操作
void sema_up(struct semaphore* psema);
//锁的初始化
void lock_init(struct lock* plock);
//获得锁
void lock_acquire(struct lock* plock);
//释放锁
void lock_release(struct lock* plock);

六:用户进程以及更细粒度内存管理模块的实现:(userprog/process.c process.h tss.c tss.h)(kernel/memory.c memory.h)
个人理解:实现用户进程是在线程的基础上去实现的,首先我们伪造好一个进程环境,然后通过一个关键的指令:iret返回到用户态。伪造用户进程环境的过程如下:创建进程的过程如下如所示:
调用的主要函数有:init_thread() ; thread_create() ; create_page_dir() ; block_desc_init()

在这里插入图片描述结果process_execute()函数创建好环境后,当调度到这个任务的时候,会执行start_process()函数,该函数会完全伪造好进入中断后的进程环境,并在最后执行iret真正返回到用户态。

接口:

void process_execute(void* filename, char* name);
void start_process(void* filename_);
void process_activate(struct task_struct* p_thread);//below three function is about page dir and vaddr//activate process page dir
void page_dir_activate(struct task_struct* p_thread);uint32_t* create_page_dir(void);//creat user process vaddr bitmap(each user process has a vaddr bitmap)
void create_user_vaddr_bitmap(struct task_struct* user_prog);

七:系统调用以及标准io的实现的实现:(userporog/syscall-init.h)(lib/user/syscall.c syscall.h) (lib/stdio.h)
先从整体上看一系统调用过程:
在这里插入图片描述

注意:早在中断模块初始化的时候就已经初始化好中断描述符当中系统调用的入口,入口如下(文件在kernel.S当中):

extern syscall_table
section .text
global syscall_handler
syscall_handler:
;1 保存上下文环境push 0                           ; 压入0, 使栈中格式统一push dspush espush fspush gspushad                           ; PUSHAD指令压入32位寄存器,其入栈顺序是:; EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI push 0x80                        ; 此位置压入0x80也是为了保持统一的栈格式;2 为系统调用子功能传入参数push edx                         ; 系统调用中第3个参数push ecx                         ; 系统调用中第2个参数push ebx                         ; 系统调用中第1个参数;3 调用子功能处理函数call [syscall_table + eax*4]     ; 编译器会在栈中根据C函数声明匹配正确数量的参数;4 将call调用后的返回值存入待当前内核栈中eax的位置mov [esp + 8*4], eaxjmp intr_exit                    ; intr_exit返回,恢复上下文

从代码中可以看到在系统调用的公共部分调用了call[syscall_table + eax*4] ,其中syscall_table是一个数组的起始地址,他是保存对应系统调用处理函数的入口地址的数组。该数组的初始化如下:(各个函数由相应的模块去实现)。

/* 初始化系统调用 */
void syscall_init(void) {put_str("syscall_init start\n");syscall_table[SYS_GETPID]        = sys_getpid;syscall_table[SYS_WRITE]         = sys_write;syscall_table[SYS_MALLOC]        = sys_malloc;syscall_table[SYS_FREE]          = sys_free;syscall_table[SYS_FORK]          = sys_fork;syscall_table[SYS_READ]          = sys_read;syscall_table[SYS_PUTCHAR]       = sys_putchar;syscall_table[SYS_CLEAR]         = cls_screen;syscall_table[SYS_GETCWD]        = sys_getcwd;syscall_table[SYS_OPEN]          = sys_open;syscall_table[SYS_CLOSE]         = sys_close;syscall_table[SYS_LSEEK]         = sys_lseek;syscall_table[SYS_UNLINK]        = sys_unlink;syscall_table[SYS_MKDIR]         = sys_mkdir;syscall_table[SYS_OPENDIR]       = sys_opendir;syscall_table[SYS_CLOSEDIR]      = sys_closedir;syscall_table[SYS_CHDIR]         = sys_chdir;syscall_table[SYS_RMDIR]         = sys_rmdir;syscall_table[SYS_READDIR]       = sys_readdir;syscall_table[SYS_REWINDDIR]     = sys_rewinddir;syscall_table[SYS_STAT]          = sys_stat;syscall_table[SYS_PS]            = sys_ps;syscall_table[SYS_EXECV]         = sys_execv;syscall_table[SYS_EXIT]          = sys_exit;syscall_table[SYS_WAIT]          = sys_wait;syscall_table[SYS_PIPE]          = sys_pipe;syscall_table[SYS_FD_REDIRECT]   = sys_fd_redirect;syscall_table[SYS_HELP]          = sys_help;put_str("syscall_init done\n");
}

八:硬盘驱动模块实现:(device/ide.c ide.h)

在这里插入图片描述

个人理解:首先,对硬盘的操作就是读(read),写(write)。那么我们手里应该有什么信息呢,或者说内核应该持有什么样的信息才能满足我们对操作硬盘的需要。首先我们要知道硬盘的结构,如下图所示:
在这里插入图片描述
其中每一个分区都有一个独立的文件系统,而每一个文件系统最重要的当然是元信息,这些元信息包括超级块,空闲块位图,inode位图,inode数组;而我们再内核中,只需要抽象出一个表示分区的数据结构即可(struct partiton),把这些信息用这个结构存起来,当要操作这个分区里的文件时,捏着这个数据结构便能解决大部分的事情。

原理:
数据结构:

/* 分区结构 */
struct partition {uint32_t start_lba;           // 起始扇区uint32_t sec_cnt;             // 扇区数struct disk* my_disk;         // 分区所属的硬盘struct list_elem part_tag;    // 用于队列中的标记char name[8];                 // 分区名称struct super_block* sb;       // 本分区的超级块struct bitmap block_bitmap;   // 块位图struct bitmap inode_bitmap;   // i结点位图struct list open_inodes;      // 本分区打开的i结点队列
};
/* 硬盘结构 */
struct disk {char name[8];                           // 本硬盘的名称,如sda等struct ide_channel* my_channel;         // 此块硬盘归属于哪个ide通道uint8_t dev_no;                         // 本硬盘是主0还是从1struct partition prim_parts[4];         // 主分区顶多是4个struct partition logic_parts[8];        // 逻辑分区数量无限,但总得有个支持的上限,那就支持8个
};/* ata通道结构 */
struct ide_channel {char name[8];                 // 本ata通道名称, 如ata0,也被叫做ide0. 可以参考bochs配置文件中关于硬盘的配置。uint16_t port_base;           // 本通道的起始端口号uint8_t irq_no;               // 本通道所用的中断号struct lock lock;bool expecting_intr;          // 向硬盘发完命令后等待来自硬盘的中断struct semaphore disk_done;   // 硬盘处理完成.线程用这个信号量来阻塞自己,由硬盘完成后产生的中断将线程唤醒struct disk devices[2];       // 一个通道上连接两个硬盘,一主一从
};

接口:

初始化:
个人理解:(:读者在阅读该部分的时候需要先熟悉通道,硬盘这些数据结构的嵌套关系)首先,初始化硬盘在我看来就是准备好一切数据,为操作服务罢了。整个硬盘的初始化,就是调用partition_scan去扫描整个硬盘的主分区以及逻辑分区,然后初始化好表示通道的数据结构channel,以及初始化好链表partition_list(所有partiton都用整个链表关联起来,用到哪个分区就从这里获得,而不必再次去硬盘中去获得相应分区的信息)
在这里插入图片描述

九:文件系统的实现:(fs/*)
个人理解:在硬盘初始化好后,我们能够得到的信息是全局的channel结构,以及一个partition_list链表,其中包含了80hd.img中所有分区的信息。
那么文件系统初始化的工作就是将每一个分区的元信息填入到硬盘相应的分区上去,这些元信息包括:超级块,inode位图,块位图等。这项工作是由函数partition_format()来实现的。其次我们还要挂载默认的分区(本质就是将cur_part指向需要操作的partiton对象),挂载完成后,需要打开该分区的根目录,以便后续操作。

设计理论
在这里插入图片描述

在这里插入图片描述

从全局上一览数据结构之间的关系:黄色代表初始化完成后内核持有的全局变量
在这里插入图片描述

原理:
数据结构:

struct file {uint32_t fd_pos;      // 记录当前文件操作的偏移地址,以0为起始,最大为文件大小-1uint32_t fd_flag;struct inode* fd_inode;
};
/* 目录结构 */
struct dir {struct inode* inode;uint32_t dir_pos;      // 记录在目录内的偏移uint8_t dir_buf[512];  // 目录的数据缓存
}; 
/* 目录项结构 */
struct dir_entry {char filename[MAX_FILE_NAME_LEN];  // 普通文件或目录名称uint32_t i_no;                     // 普通文件或目录对应的inode编号enum file_types f_type;            // 文件类型
}; 
/* inode结构 */
struct inode {uint32_t i_no;    // inode编号/* 当此inode是文件时,i_size是指文件大小,
若此inode是目录,i_size是指该目录下所有目录项大小之和*/uint32_t i_size;uint32_t i_open_cnts;   // 记录此文件被打开的次数bool write_deny;        // 写文件不能并行,进程写文件前检查此标识/* i_sectors[0-11]是直接块, i_sectors[12]用来存储一级间接块指针 */uint32_t i_sectors[13];struct list_elem inode_tag;
}; 

接口:

初始化:
在这里插入图片描述

十:shell的实现以及系统调用的进一步丰富:(shell/) (userprog/)


http://chatgpt.dhexx.cn/article/wapi2c0R.shtml

相关文章

还原卡还原了数据能恢复吗_主动还原:我们可以更快地恢复吗? 快多了?

还原卡还原了数据能恢复吗 Hi! My name is Daulet Tymbayev, and today I want to share my experience of developing a system that (theoretically) is able to recover a disk much faster than traditional recovery. Let’s start from the beginning to cover all the p…

纯html 404页面,一款纯css3实现的漂亮的404页面_html/css_WEB-ITnose

一款纯css3实现的漂亮的404页面_html/css_WEB-ITnose 之前为大家分享了那些创意有趣的404页面, html5和css3打造一款创意404页面, HTML5可爱的404页面动画很逗的机器人。今天再给大家分享一款纯css3实现的漂亮的404页面。效果图如下: 在线预…

VUE 404页面的实现

效果图&#xff1a; 实现方式&#xff1a; 创建一个404的vue页面 <template><div class"wscn-http404-container"><div class"wscn-http404"><div class"pic-404"><imgclass"pic-404__parent"src"/…

Django配置404页面

目录 一.settings配置二.url设置三.views中设置四.最后附一个404页面的模板。 一.settings配置 1.首先需要在settings中将DEBUG由原来的True改为False DEBUG False2.需要设置 ALLOWED_OSTS ["*"]二.url设置 三.views中设置 def page_not_found(request,**kwarg…

收集的几个自定义的404页面的模板

文章目录 404pages : HTTP Status 404 – Not Found介绍&#xff1a;404页面分类&#xff1a;补充内容&#xff1a;致谢&#xff1a;声明 404pages : HTTP Status 404 – Not Found 介绍&#xff1a; 注&#xff1a;项目已上传到github&#xff1a;https://github.com/yanshen…

网站怎么自定义404页面

404页面是客户端在浏览网页时&#xff0c;服务器无法正常提供信息&#xff0c;或是服务器无法回应&#xff0c;且不知道原因所返回的页面&#xff0c;简单的说就是当别人访问你网站的某一个已经删除或不存在的网页的时候&#xff0c;服务器自动显示的页面。404页面对seo是有一定…

vue 中编写404页面

前言 今日给自己项目添加404页面时&#xff0c;踩了一点坑&#xff0c;分享给大家。 正文 <div class"_404"><h2 class"m-0">抱歉&#xff0c;页面未找到&#xff0c;<span>{{countDown}}</span>s后自动跳转到<a href"j…

hexo自定义404页面

1.找到你中意的404页面 推荐一款&#xff1a;https://404.life 预览保存&#xff0c;如果你看到其他喜欢的页面如何F12扒页面我就不教咯~ 2.hexo配置 2.1 把css和js放在博客下 我是放在这儿的&#xff0c;你也可以放在github&#xff0c;用jsd加速。 2.2 新建404.html 路…

缤纷多彩的404页面(404.html)

文章来源&#xff1a;https://www.skyqian.com/archives/404-Pages.html 一般而言&#xff0c;第一时间会在博客更新&#xff0c;CSDN随缘更新。 引言 别离滋味浓于酒。著人瘦。此情不及墙东柳。春色年年如旧。 ——勿埋我心 404是个很常见的页面&#xff0c;当该网站的内容搬迁…

html404页面怎么添加,网站要如何设置自定义404页面?

之前我们讲述过网站设置404页面对于优化或是用户体验的重要意义&#xff0c;大家可移步到《网站为什么要设置404页面》查看&#xff0c;今天我们讲解的是网站要如何设置自己的404页面。 现在大多数空间商都有了404设置的功能&#xff0c;我们可将404页面上传至空间里面&#xf…

phpstudy 404页面设置 也就是Apache404页面的设置

1&#xff0c;让apache支持.htaccess 我们要找到apache安装目录下的httpd.conf文件,在里面找到 <Directory /> Options FollowSymLinks AllowOverride none </Directory> 我们只要把蓝色字的none改all就重起apache就好了 如图&#xff1a; 2、找到httpd.conf文件…

个性404页面模板php,25个创意404页面,支持模板下载

对于SEOer来讲,一个好的404页面是很有必要的,既要让蜘蛛能识别出不需要的网页,也要让用户在访问到错误页面时感觉舒服,所以,在选择404页面时SEOer就纠结了,到底什么样的一款404页面是好的呢?或者说是合适的呢? 今天Kane就在这里整理了26款404页面,分别是五个类别,大部…

vue 页面跳转404_出现404页面怎么办?应该如何处理404页面?

当出现404页面怎么办?应该如何处理404页面?我们都知道404页面是用户在输入错误的链接时,显示的返回页面,但是作为SEOer来说,一切与用户有关的事,我们都不能忽略, 当用户已经点击进来,难道要因为出现的404页面而失去众多用户吗?不!这不是一个优秀的SEOer该做的事,下面…

html5 小游戏 404页面,原来404页面可以这样做

404页面是网站必备的一个页面,它承载着用户体验与SEO优化的重任。404页面通常为用户访问了网站上不存在或已删除的页面,服务器返回的404错误。如果站长没有设置404页面,会出现死链接,蜘蛛爬行这类网址时,不利于搜索引擎收录。 设置404页面的两大好处 1:引导用户不要关闭网…

SpringBoot 配置404页面

SpringBoot 配置404页面 项目环境&#xff1a; 服务器&#xff1a;centos 前端&#xff1a;Vue 后端&#xff1a;SpringBoot 出现的问题 访问一个不存在的页面时&#xff0c;会出现tomcat自带的404界面&#xff0c;这个界面对于用户不太友好 解决方案 1、自己写好404页面…

vue 404页面

一、概述 如果用户输入错误的网址没有提示&#xff0c;界面也不会有任何变化&#xff0c;用户体验非常不友好&#xff0c;所以需要设置错误提示 二、设置 设置404页面需要在配置路由文件index.js中设置&#xff0c;其中 * 代表的就是404页面 // 404 page must be placed at the…

自适应404页面

404.CSS *{ padding:0;margin:0;box-sizing:border-box;font-family:"微软雅黑";} body,html{width:100%;height:100%;} .container{max-width:90%;margin:0 auto;padding:80px 0px; } img{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;us…

什么是404页面,404有什么做用,网站做404有什么好处?

什么是404页面&#xff1f; 404页面是客户端在浏览网页时&#xff0c;服务器无法正常提供信息&#xff0c;又不知情况&#xff0c;所返回的页面。很多用户访问错误网页时就会经常出现这个404页面。 所谓404页面主要指的是在运营网站的过程中&#xff0c;当用户点击了某个网页…

网页报错404原因及解决方法

网页报错404&#xff1a;即找不到该资源 未开启服务 若使用的是tomcat服务器&#xff0c;先检查服务器有没有正常启动&#xff0c;网络连接是否正常。 服务器未正确部署 使用开发工具为idea&#xff0c;检查tomcat在idea是否部署正确。 服务器配置出错 tomcat在idea中的配置出错…

404是什么意思,404错误页面有什么用?

在我们浏览网页时&#xff0c;时常会出现一些404页面&#xff0c;导致无法正常浏览网页&#xff0c;那么什么是404呢&#xff0c;为什么会产生404页面呢&#xff1f;下面我们一起来看看。 404介绍 404其实是一种http状态码&#xff0c;代表用户在浏览网页时&#xff0c;服务器…