linux期中总结
2016-04-22 13:23
477 查看
1. Linux内核启动的过程——以MenuOS为例
1.1 计算机的启动过程
CPU启动后,BIOS程序开始执行,检测硬件,然后加载引导程序BootLoader和硬盘的第一个扇区MBR。BootLoader会将操作系统初始化,启动操作系统。
Linux内核的启动有三个参数:
kernel
initrd
root所在目录、分区。
内核会首先生成0号进程idle,然后0号进程产生1号进程init,1号进程是所有用户态进程的祖先,0号进程是所有内核线程的祖先。
1号进程是Linux启动后所执行的第一个进程;
0号进程是当没有进程可以执行时,会执行0号进程。
1.2 进程的描述
进程的描述依靠进程描述符task_struct数据结构,部分定义如下:struct task_struct { volatile long state; /* 进程状态 */ void *stack; /* 进程的内核堆栈 */ atomic_t usage; unsigned int flags; /* 每个进程的标识符 */ unsigned int ptrace; / #ifdef CONFIG_SMP // 条件编译,SMP多处理器相关 …… int on_rq // 运行队列相关,下面几行是进程队列和调度相关。 …… struct list_head tasks // 进程链表 …… next_task prev_task // 对进程链表的管理 tty_struct // 控制台 fs_struct // 文件系统 struct files_struct *files; // 打开的文件描述符列表 file_struct // 打开的文件描述符 mm_struct // 内存管理描述 struct mm_struct *mm, *active_mm; // 地址空间,内存管理。 signal_struct // 进程间通信、信号描述 struct list_head ptraced // 调试用 utime stime // 进程时间相关 }
进程的唯一标示是pid。
1.3 进程的创建
创建进程,其本质是调用fork函数创建一个子进程,从1号进程开始,都是使用的这种方法。Linux通过clone()系统调用实现fork()。
创建进程的大概步骤如下:
fork()、vfork()、__clone()都根据各自需要的参数标志调用clone()。
由clone()去调用do_fork()。
do_fork()调用copy_process()函数,然后让进程开始运行。
返回do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。
部分代码示意如下:
/* 复制一个PCB */ err = arch_dup_task_struct(tsk, orig); /* 要给新进程分配一个新的内核堆栈 */ ti = alloc_thread_info_node(tsk, node); tsk->stack = ti; setup_thread_stack(tsk, orig); //这里只是复制thread_info,而非复制内核堆栈 /* 要修改复制过来的进程数据,比如pid、进程链表等 */ *childregs = *current_pt_regs(); //复制内核堆栈 childregs->ax = 0; //eax设置为0,所以子进程返回值为0. p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶 p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址 dup_task_struct // 复制pcb alloc_thread_info_node // 创建了一个页面,其实就是实际分配内核堆栈空间的效果。 setup_thread_stack // 把thread_info的东西复制过来
也就是说,子进程的创建绝大部分内容是直接复制了父进程的进程描述符,这是它们有共同的地址空间,堆栈,和进程上下文,当写时复制的时候才会区分开。
最开始学习fork的时候提到,fork是“一次调用,两次返回”,父进程返回子进程的pid,子进程返回0,这是因为子进程复制父进程相关数据的时候,将eax设置为0。
除此之外,还有特殊设置的是sp和ip,sp指向调度到子进程时的内核栈顶,ip指向ret_from_fork,子进程就是从这条指令开始执行的。
1.4 进程调度
Linux采用的是CFS调度算法,有四个组成部分:Linux的调度基于分时和优先级。
Linux的进程根据优先级排队
根据特定的算法计算出进程的优先级,用一个值表示
这个值表示把进程如何适当的分配给CPU
Linux进程中的优先级是动态的
调度程序会根据进程的行为周期性地调整进程的优先级
例如:
较长时间为被分配到cpu——↑
已经在cpu上运行了较长时间——↓
常见的一些函数:
nice getpriority/setpriority 设置优先级 sched_getschedduler/sched_setscheduler sched_getparam/sched_setparam sched_yield sched_get_priority_min/sched_get_priority_max sched_rr_get_interval
进程调度的时机
中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();主动调度。
用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。用户态进程只能被动调度。
内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程既可以主动调度,也可以被动调度;
内核线程是只有内核态没有用户态的特殊进程
1.5 抢占和上下文切换
need_resched标志:
内核用这个标志来表明是否需要重新执行一次调度。
当某个进程应该被抢占时,scheduler_tick()会设置这个标志。
当一个优先级高的进程进入可执行状态时,try_to_wake_up()会设置这个标志。
内核检查这个标志确认其被设置,调用schedule()来切换到一个新的进程。
该标志对于内核来说是一个信息,表示youqitajinc应当被运行了,要尽快调用调度程序。
再返回用户空间以及从中断返回时,内核也会检查标志。
每个进程都包含一个need_resched标志,因为访问进程描述符里的数值比访问一个全局变量要快。
锁是非抢占区域的标志。实现如下:
为每个进程的thread_info中加入preempt_count计数器,初值为0,使用锁+1,释放锁-1,数值为0时,可以执行抢占。
挂起正在CPU上执行的进程,与中断时保存现场是不同的,中断前后是在同一个进程上下文中,只是由用户态转向内核态执行,但是是同一个进程,而进程上下文的切换是两个进程在切换。
进程上下文包含了进程执行需要的所有信息
用户地址空间:包括程序代码,数据,用户堆栈等
控制信息:进程描述符,内核堆栈等
硬件上下文(注意中断也要保存硬件上下文只是保存的方法不同)
schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换。
2.可执行程序的装载
2.1 预处理、编译、链接和目标文件的格式
查看一个可执行文件头部内容:
readelf -h
头部后是代码和数据,等等。
可执行程序加载的主要工作:
当创建或者增加一个进程映像的时候,系统在理论上将拷贝一个文件的段到一个虚拟的内存段
静态链接的ELF可执行文件和进程的地址空间
32位x86进程地址空间共4G,1G是内核空间。
如何加载到内存?
默认从0x8048000开始加载,然后头部需要占用一定空间,程序的实际入口可以在0x8048100等地方,即可执行文件加载到内存中开始执行的第一行代码的入口处。
一般静态链接会把所有代码放在一个代码段,
动态链接会有多个代码段。
2.2 可执行程序的装载
2.2.1 sys_execve的内部处理过程
1604 SYSCALL_DEFINE3(execve, 1605 const char __user *, filename, 1606 const char __user *const __user *, argv, 1607 const char __user *const __user *, envp) 1608 { 1609 return do_execve(getname(filename), argv, envp); 1610 } 1611 #ifdef CONFIG_COMPAT 1612 COMPAT_SYSCALL_DEFINE3(execve, const char __user *, filename, 1613 const compat_uptr_t __user *, argv, 1614 const compat_uptr_t __user *, envp) 1615 { 1616 return compat_do_execve(getname(filename), argv, envp); 1617 } 1618 #endif
sys_execve函数中返回了一个do_execve:
1549 int do_execve(struct filename *filename, 1550 const char __user *const __user *__argv, 1551 const char __user *const __user *__envp) 1552 { 1553 struct user_arg_ptr argv = { .ptr.native = __argv }; 1554 struct user_arg_ptr envp = { .ptr.native = __envp }; 1555 return do_execve_common(filename, argv, envp); 1556 } 最后一句中do_execve_common把文件名,参数和环境转换了一下。
该函数do_execve_common打开如下:
1474 file = do_open_exec(filename); 打开了一个要加载的可执行文件,然后会加载一下它的头部,建立一个结构体,把命令行参数和环境变量拷贝到结构体中; 1513 retval = exec_binprm(bprm); 对这个可执行文件的处理过程。
打开exec_binprm这个函数,可以找到一句重要代码:
1416 ret = search_binary_handler(bprm); 寻找这个我们打开的可执行文件的处理函数。
打开search_binary_handler,找到list_for_each_entry如下:
1369 list_for_each_entry(fmt, &formats, lh) { 1370 if (!try_module_get(fmt->module)) 1371 continue; 1372 read_unlock(&binfmt_lock); 1373 bprm->recursion_depth++; 1374 retval = fmt->load_binary(bprm); 1375 read_lock(&binfmt_lock); 1376 put_binfmt(fmt); 1377 bprm->recursion_depth--; 1378 if (retval < 0 && !bprm->mm) { 1379 /* we got to flush_old_exec() and failed after it */ 1380 read_unlock(&binfmt_lock); 1381 force_sigsegv(SIGSEGV, current); 1382 return retval; 1383 } 1384 if (retval != -ENOEXEC || !bprm->file) { 1385 read_unlock(&binfmt_lock); 1386 return retval; 1387 } 1388 } 在这个循环里寻找能够解析这个当前可执行文件的代码模块。 retval = fmt->load_binary(bprm); // 这一句中的load_binary,加载处理函数。这一句是函数指针,实际上是调用的load_elf_binary。
2.2.2 load_elf_binary的赋值和注册
/* 全局变量elf_format,把函数指针load_elf_binary**赋值**给了.load_binary */ 82 static struct linux_binfmt elf_format = { 83 .module = THIS_MODULE, 84 .load_binary = load_elf_binary, 85 .load_shlib = load_elf_library, 86 .core_dump = elf_core_dump, 87 .min_coredump = ELF_EXEC_PAGESIZE, 88 }; /* 把变量elf_format**注册**进了format链表里,就可以在链表里对应elf模式中找到对应模块 */ 2198 static int __init init_elf_binfmt(void) 2199 { 2200 register_binfmt(&elf_format); 2201 return 0; 2202 }
在load_elf_binary中调用了start_thread这个函数:
198 start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)/* pt_regs 是内核堆栈栈底的函数,*/ 199 { 200 set_user_gs(regs, 0); 201 regs->fs = 0; 202 regs->ds = __USER_DS; 203 regs->es = __USER_DS; 204 regs->ss = __USER_DS; 205 regs->cs = __USER_CS; 206 regs->ip = new_ip; //起点位置 207 regs->sp = new_sp; 208 regs->flags = X86_EFLAGS_IF; 209 /* 210 * force it to the iret return path by making it look as if there was 211 * some work pending. 212 */ 213 set_thread_flag(TIF_NOTIFY_RESUME); 214 } 215 EXPORT_SYMBOL_GPL(start_thread);
2.2.3 动态链接与静态链接
动态链接库的执行过程:887 if (elf_interpreter) { 888 unsigned long interp_map_addr = 0; 889 890 elf_entry = load_elf_interp(&loc->interp_elf_ex, 891 interpreter, 892 &interp_map_addr, 893 load_bias); 需要加载连接器
静态链接的执行过程:
912 else { 913 elf_entry = loc->elf_ex.e_entry; 直接把elf文件的entry地址赋给elf_entry。
但是在start_thread中是直接用的elf_entry:
start_thread(regs,elf_entry, bprm->p); 1.如果是一个静态连接的文件,elf_entry就是指的main函数开始的位置 2.如果是一个需要依赖动态链接库的文件,elf_entry指向的是动态链接器的起点,将cpu控制权交给ld来加载依赖库并完成动态链。
3. 系统调用
3.1 用户态和内核态
用户通过库函数与系统调用联系起来。内核态
在高执行级别下,代码可以执行特权指令,访问任意的物理地址。
用户态:
代码的掌控范围受到限制。
intel x86 CPU有四个权限分级,0-3。Linux只取两种,0是内核态,3是用户态
区分权限级别使得系统更加稳定。
如何区分用户态与内核态?
cs:eip。[代码段选择寄存器:偏移量寄存器]
通过cs寄存器的最低两位,表示当前代码的特权级:
【针对逻辑地址】
0xc0000000以上的空间只能在内核态下访问
0x00000000-0xbfffffff两种状态下都可以访问
如何进行切换?
中断。
3.2 中断处理过程
中断处理是从用户态进入内核态的主要方式。寄存器上下文
从用户态切换到内核态时,必须保存用户态的寄存器上下文到内核堆栈中,同时会把当前内核态的一些信息加载,例如cs:eip指向中断处理程序入口。
中断发生后的第一件事就是保存现场 - SAVE_ALL
中断处理结束前最后一件事是恢复现场 - RESTORE_ALL
3.3 系统调用的“三张皮”
3.4 系统调用在内核代码中的处理过程
以system_call为例:trap_init里面有一个set_system_trap_gate函数,函数定义中有系统调用的中断向量SYSCALL_VECTOR和汇编代码入口system_call。
一旦执行int 0x80,系统直接跳转到system_call来执行。
从以上可以看出:
在系统调用返回之前,可能发生进程调度,进程调度里就会出现进程上下文的切换
进程间通信可能有信号需要处理
相关文章推荐
- 让linux每天定时备份MySQL数据库并删除五天前的备份文件
- 制作一个自己的linux光盘(使用kickstart)
- 实习过程中linux相关开发学习总结(四)
- Linux vi中查找字符内容的方法
- linux
- Linux/CentOS学习记录
- CentOS 7安装zabbix-2.4.8监控
- OpenCV在Windows、Linux、Android、iOS上的安装
- linux解压覆盖命令
- Centos 6.5 安装icingaweb1 中文版部署
- VMware和CentOS7安装和配置
- linux下查看java 占用cpu使用情况
- linux软件安装与卸载
- Linux基础入门(1):用户及文件权限管理
- 关于linux上文件无法正确显示中文的情况解决
- rcInsDriver
- Linux安装卸载MySQL以及修改MySQL初始密码
- Linux下OpenSSL客户端中使用req命令来生成证书的教程
- linux下查找包含BOM头的文件和清除BOM头命令
- Centos 日志处理