linux系统分析第三次实验

进程创建——task_struct

学号413

原创博客,转载请注明出处+中国科学技术大学孟宁老师的Linux操作系统分析 https://github.com/mengning/linuxkernel/

进程是处于执行期的程序以及它所管理的资源(如打开的文件、挂起的信号、进程状态、地址空间等等)的总称。首先在linux操作系统下,当你触发任何一个事件时,系统都将它定义为一个进程,并且给予这个进程一个ID,即PID。
那么如何产生一个进程呢?简单来说就是“执行一个程序或命令”。
Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息。它定义在linux-2.6.38.8/include/linux/sched.h文件中。

进程和父进程

一个进程创建的另一个新进程称为子进程。相反地,创建子进程的进程称为父进程。
对于一个普通的用户进程,它的父进程就是执行它的哪个Shell,对于Linux而言,Shell就是bash。

fork和exec

进程相互之间存在着调用,在Linux的过程调用通常称为fork-and-exec流程,首先进程都会通过父进程以复制(fork)的形式产生一个一模一样的子进程,然后复制出来的子进程通过exec的方式来执行实际要进行的进程,子进程复制/拷贝父进程的PCB、数据空间(数据段、堆和栈),父子进程共享正文段(只读),父子进程执行完fork以后会继续执行fork后的代码。
子进程与父进程唯一的区别就是PID,并且子进程多了一个PPID参数。

我们来看下面一段代码:

#include<stdio.h>

int main()
{
    fork();
    printf("I am process!");
    return 0;
}

它fork()一共有三种不同的返回值:
1、在父进程中,fork返回新创建子进程的进程ID;
2、在子进程中,fork返回0;
3、如果出现错误,fork返回一个负值;

一个进程中的所有相关信息都在一个task_struct结构体保存。
下面来看一下内部的函数成员:

调度成员函数

volatile long states 表示进程的当前状态

unsigned long flags 进程标志

long priority 进程优先级

unsigned long rt_priority

实时进程的优先级,rt_priority+1000给出进程每次获取CPU后可使用的时间(同样按jiffies计)。实时进程的优先级可通过系统 调用sys_sched_setscheduler()改变

long counter 轮转法调度时表示进程当前还可运行多久

unsigned long policy 进程的进程调度策略

进程标识

(1) unsigned short uid,gid;
uid和gid是运行进程的用户标识和用户组标识。

(2) int groups[NGROUPS];
与多数现代UNIX操作系统一样,Linux允许进程同时拥有一组用户组号。在进程访问文件时,这些组号可用于合法性检查。

(3) unsigned short euid,egid;
euid 和egid又称为有效的uid和gid。出于系统安全的权限的考虑,运行程序时要检查euid和egid的合法性。通常,uid等于euid,gid等于 egid。有时候,系统会赋予一般用户暂时拥有root的uid和gid(作为用户进程的euid和egid),以便于进行运作。

(4) unsigned short fsuid,fsgid;
fsuid 和fsgid称为文件系统的uid和gid,用于文件系统操作时的合法性检查,是Linux独特的标识类型。它们一般分别和euid和egid一致,但在 NFS文件系统中NFS服务器需要作为一个特殊的进程访问文件,这时只修改客户进程的fsuid和fsgid。

(5) unsigned short suid,sgid;
suid和sgid是根据POSIX标准引入的,在系统调用改变uid和gid时,用于保留真正的uid和gid。

(6) int pid,pgrp,session;
进程标识号、进程的组织号及session标识号,相关系统调用(见程序kernel/sys.c)有sys_setpgid、sys_getpgid、sys_setpgrp、sys_getpgrp、sys_getsid及sys_setsid几种。

(7) int leader;
是否是session的主管,布尔量。

时间数据成员

(1) unsigned long timeout;
用于软件定时,指出进程间隔多久被重新唤醒。采用tick为单位。

(2) unsigned long it_real_value,it_real_iner;
用 于itimer(interval timer)软件定时。采用jiffies为单位,每个tick使it_real_value减到0时向进程发信号SIGALRM,并重新置初值。初值由 it_real_incr保存。具体代码见kernel/itimer.c中的函数it_real_fn()。

(3) struct timer_list real_timer;
一种定时器结构(Linux共有两种定时器结构,另一种称作old_timer)。数据结构的定义在include/linux/timer.h中,相关操作函数见kernel/sched.c中add_timer()和del_timer()等。

(4) unsigned long it_virt_value,it_virt_incr;
关 于进程用户态执行时间的itimer软件定时。采用jiffies为单位。进程在用户态运行时,每个tick使it_virt_value减1,减到0时 向进程发信号SIGVTALRM,并重新置初值。初值由it_virt_incr保存。具体代码见kernel/sched.c中的函数 do_it_virt()。

(5) unsigned long it_prof_value,it_prof_incr;
同样是 itimer软件定时。采用jiffies为单位。不管进程在用户态或内核态运行,每个tick使it_prof_value减1,减到0时向进程发信号 SIGPROF,并重新置初值。初值由it_prof_incr保存。 具体代码见kernel/sched.c中的函数do_it_prof。

(6) long utime,stime,cutime,cstime,start_time;
以上分别为进程在用户态的运行时间、进程在内核态的运行时间、所有层次子进程在用户态的运行时间总和、所有层次子进程在核心态的运行时间总和,以及创建该进程的时间。

信号量数据成员

(1) struct sem_undo *semundo;
进 程每操作一次信号量,都生成一个对此次操作的undo操作,它由sem_undo结构描述。这些属于同一进程的undo操作组成的链表就由semundo 属性指示。当进程异常终止时,系统会调用undo操作。sem_undo的成员semadj指向一个数据数组,表示各次undo的量。结构定义在 include/linux/sem.h。

(2) struct sem_queue *semsleeping;
每一信号量集合对应一 个sem_queue等待队列(见include/linux/sem.h)。进程因操作该信号量集合而阻塞时,它被挂到semsleeping指示的关 于该信号量集合的sem_queue队列。反过来,semsleeping。sleeper指向该进程的PCB。

进程上下文环境

(1) struct desc_struct *ldt;
进程关于CPU段式存储管理的局部描述符表的指针,用于仿真WINE Windows的程序。其他情况下取值NULL,进程的ldt就是arch/i386/traps.c定义的default_ldt。

(2) struct thread_struct tss;
任务状态段,其内容与INTEL CPU的TSS对应,如各种通用寄存器.CPU调度时,当前运行进程的TSS保存到PCB的tss,新选中进程的tss内容复制到CPU的TSS。结构定义在include/linux/tasks.h中。

(3) unsigned long saved_kernel_stack;
为MS-DOS的仿真程序(或叫系统调用vm86)保存的堆栈指针。

(4) unsigned long kernel_stack_page;
在内核态运行时,每个进程都有一个内核堆栈,其基地址就保存在kernel_stack_page中。

do_fork系统调用创建新进程的过程如下:
fork()----->system_call()---->sysfork()---->do_fork()---->copy_process()---->新进程创建

编译链接的过程和ELF可执行文件格式;

就我们的第一个程序为例:

#include <stdio.h>

int main(int argc, char* argv[])
{
    printf("Hello World!\n");
    return 0;
}

从源文件xxx.c编译链接成xxx.exe,需要经历如下步骤
程序的编译链接过程
ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西。ELF文件格式提供了两种视图,分别是链接视图和执行视图。
ELF

对LINUX加载可执行程序所需的过程的理解

程序的加载,涉及到两个工具,linker 和loader。Linker主要涉及动态链接库的使用,loader主要涉及软件的加载。

1、 exec执行一个程序

2、 elf为现在非常流行的可执行文件的格式,它为程序运行划分了两个段,一个段是可以执行的代码段,它是只读,可执行;另一个段是数据段,它是可读写,不能执行。

3、 loader会启动,通过mmap系统调用,将代码端和数据段映射到内存中,其实也就是为其分配了虚拟内存,注意这时候,还不占用物理内存;只有程序执行到了相应的地方,内核才会为其分配物理内存。

4、 loader会去查找该程序依赖的链接库,首先看该链接库是否被映射进内存中,如果没有使用mmap,将代码段与数据段映射到内存中,否则只是将其加入进程的地址空间。这样比如glibc等库的内存地址空间是完全一样。

运行过程中链接动态链接库与编译过程中链接动态库的区别。

我们调用动态链接库有两种方法:一种是编译的时候,指明所依赖的动态链接库,这样loader可以在程序启动的时候,来所有的动态链接映射到内存中;一种是在运行过程中,通过dlopen和dlfree的方式加载动态链接库,动态将动态链接库加载到内存中。

这两种方式,从编程角度来讲,第一种是最方便的,效率上影响也不大,在内存使用上有些差别。
第一种方式,一个库的代码,只要运行过一次,便会占用物理内存,之后即使再也不使用,也会占用物理内存,直到进程的终止。
第二中方式,库代码占用的内存,可以通过dlfree的方式,释放掉,返回给物理内存。

使用gdb跟踪do_fork

menuos和qemu的调已在上次实验中进行了演示,使用下列命令启动qemu:

qemu-system-i386 -kernel linux-5.0/arch/x86/boot/bzImage -initrd rootfs.img -S -s -append nokaslr 

然后在gdb中跟踪do_fork过程,结果如下:
do_fork在do_fork处设置断点,图中可以看到copy_process函数的执行。

整个do_fork()大概过程:

fork() -> sys_clone() -> do_fork() -> dup_task_struct() -> copy_process() -> copy_thread() -> ret_from_fork()

总结:

创建进程过程中要做哪些事情:修改pcb,建立链表,修改分配内核堆栈,保存进程执行到哪个位置,保存sp、ip(避免发生混乱),需要有thread设定eip和esp位置。
创建新进程在内核中执行的过程:
1.复制一个PCB——task_struct
2.给新进程分配一个新的内核堆栈
3.更改复制过来的进程数据,比如pid,进程链表等
系统调用内核处理函数:sys_fork、sys_clone、sys_vfork
do_fork()中包含的copy process创建一个进程内容的主要代码
arch dup_task_struct复制整个PCB(dst=src数据结构的值复制给dst)

linux系统中,可执行程序一般要经过预处理、编译、汇编、链接、执行等步骤。
c代码,经过预处理,变成汇编代码;经过汇编器,变成目标代码;连接成可执行文件;加载到内核中执行。
ELF文件格式中有三种主要的文件格式:
(1)可重定位文件
主要是.o文件,保存有代码和适当数据,用来和其他的object文件一起来创建一个可执行文件或者共享文件
(2)可执行文件
保存着一个用来执行的程序,该文件指出exec(BA_OS)如何创建程序进程映象。
(3)共享目标文件
保存代码和合适的数据,用来和链接器链接:
  可执行程序加载的主要工作:当创建或者增加一个进程映像的时候,系统在理论上将拷贝一个文件的段到一个虚拟的内存段。
Linux中,内核线程可以主动调度,主动调度时不需要中断上下文的切换。Linux内核调用schedule()函数进行调度,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值