学号尾数:363
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
关于task_struct数据结构
每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。task_struct是Linux内核的一种数据结构,它会被装载到RAM中并且包含着进程的信息。每个进程都把它的信息放在 task_struct 这个数据结构体,task_struct 部分内容如下:
struct task_struct{ pid_t pid; //进程id uid_t uid,euid; gid_t gid,egid; volatile long state; //进程状态,0 running(运行/就绪);1/2 均等待态,分别响应/不响应异步信号;
//4 僵尸态,Linux特有,为生命周期已终止,但PCB未释放;8 暂停态,可被恢复 int exit_state; //退出的状态 unsigned int rt_priority; //调度优先级 unsigned int policy; //调度策略 struct list_head tasks; struct task_struct *real_parent; struct task_struct *parent; struct list_head children,sibling; struct fs_struct *fs; //进程与文件系统管理,进程工作的目录与根目录 struct files_struct *files; //进程对所有打开文件的组织,存储指向文件的句柄们 struct mm_struct *mm; //内存管理组织,存储了进程在用户空间不同的地址空间,可能存的数据,可能代码段 struct signal_struct *signal; //进程间通信机制--信号 struct sighand_struct *sighand; //指向进程 cputime_t utime, stime; //进程在用户态、内核态下所经历的节拍数 struct timespec start_time; //进程创建时间 struct timespec real_start_time; //包括睡眠时间的创建时间 }
关于do_fork()函数调用
Linux中创建进程有三个函数fork(),vfork()和clone(),do_fork()函数对应其系统调用。调用copy_process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息之后将子进程压入队列,等待被调度。以下为do_fork()函数部分代码:
long do_fork(unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr) { struct task_struct *p; int trace = 0; long nr; // ... // 复制进程描述符,返回创建的task_struct的指针 p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace); if (!IS_ERR(p)) { struct completion vfork; struct pid *pid; trace_sched_process_fork(current, p); // 取出task结构体内的pid pid = get_task_pid(p, PIDTYPE_PID); nr = pid_vnr(pid); if (clone_flags & CLONE_PARENT_SETTID) put_user(nr, parent_tidptr); // 如果使用的是vfork,那么必须采用某种完成机制,确保父进程后运行 if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); get_task_struct(p); } // 将子进程添加到调度器的队列,使得子进程有机会获得CPU wake_up_new_task(p); // ... // 如果设置了 CLONE_VFORK 则将父进程插入等待队列,并挂起父进程直到子进程释放自己的内存空间 // 保证子进程优先于父进程运行 if (clone_flags & CLONE_VFORK) { if (!wait_for_vfork_done(p, &vfork)) ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid); } put_pid(pid); } else { nr = PTR_ERR(p); } return nr; }
通过gdb跟踪系统调用内核处理函数do_fork()
输入命令:
rm menu -rf git clone https://github.com/mengning/menu.git cd menu mv test_fork.c test.c make rootfs
然后进行gdb调试:
b sys_clone
b _do_fork
b dup_task_struct
b copy_process
调试效果如上图所示。
理解编译链接的过程和ELF可执行文件格式
1、 编译与链接的过程:
从源代码到可执行程序所要经历的过程概述如下图:
源代码(.c .cpp .h)经过c预处理器(cpp)后生成.i文件,编译器(cc1、cc1plus)编译.i文件后生成.s文件,汇编器(as)汇编.s文件后生成.o文件,链接器(ld)链接.o文件生成可执行文件。
2、 ELF可执行文件
ELF文件格式包括三种主要的类型:可执行文件、可重定向文件、共享库。
1.可执行文件(应用程序)可执行文件包含了代码和数据,是可以直接运行的程序。
2.可重定向文件(.o)可重定向文件又称为目标文件,它包含了代码和数据(这些数据是和其他重定位文件和共享的object文件一起连接时使用的)。
.o文件参与程序的连接(创建一个程序)和程序的执行(运行一个程序),它提供了一个方便有效的方法来用并行的视角看待文件的内容,这些.o文件的活动可以反映出不同的需要。
Linux下,我们可以用gcc -c编译源文件时可将其编译成.o格式。
3.共享文件(*.so)也称为动态库文件,它包含了代码和数据(这些数据是在连接时候被连接器ld和运行时动态连接器使用的)。动态连接器可能称为ld.so.1,libc.so.1或者 ld-linux.so.1。
实验总结
所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。
源代码(.c .cpp .h)经过c预处理器(cpp)后生成.i文件,编译器(cc1、cc1plus)编译.i文件后生成.s文件,汇编器(as)汇编.s文件后生成.o文件,链接器(ld)链接.o文件生成可执行文件。