打开电脑的任务管理器:最直观的进程
操作系统的分层结构
首先提出三个问题
- 进程的创建过程?
- 进程的执行过程??
- 进程的销毁过程???
32位系统的虚拟地址空间 上图就是进程的虚拟地址空间,它并不是内存!!而是一个结构体(mm_struct)。
进程概述 可执行程序:是静态的,就是个存放在磁盘里的 可执行文件,就是一系列的指令集合。 进程(Process):是动态的,是程序的一次执行过程,同一个程序多次执行会对应 多个进程
虚拟地址与物理地址的关系
对于每一个可执行程序,其在经历预处理、编译、汇编之后,都要经过链接器将其链接成一个单一的可执行文件。 当执行这个可执行程序时,
#include <stdio.h>
#include <stdlib.h>
int bss_var;
int data_var0 = 1;
int main()
{
printf("Test location:\n");
printf("\tAddress of main(Code Segment):%p\n",main);
printf("_____________________________________\n");
int stack_var0 = 2;
printf("Stack location:\n");
printf("\tInitial end of stack:%p\n",&stack_var0);
int stack_var1 = 3;
printf("\tNew end of stack:%p\n",&stack_var1);
printf("_____________________________________\n");
printf("Data location:\n");
printf("\tAddress of data_var(Data Segment):%p\n",&data_var0);
static int data_var1 = 4;
printf("\tNew end of data_var(Data Segment):%p\n",&data_var1);
printf("_____________________________________\n");
printf("BSS location:\n");
printf("\tAddress of bss_var:%p\n",&bss_var);
printf("_____________________________________\n");
printf("Heap location:\n");
char *p = (char *)malloc(10);
printf("\tAddress of head_var:%p\n",p);
return 0;
}
这个地址是虚拟地址,其实在我们编译的时候,这些地址就已经确定了,不管执行多少次,地址都不会改变
进一步解析
Linux提供了三个系统调用用于创建进程,分别是fork,vfork,clone: 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;
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
}
copy_process()函数 部分代码
static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
int retval;
struct task_struct *p;
p = dup_task_struct(current);
}
dup_task_struct()函数 部分代码
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
unsigned long *stackend;
int node = tsk_fork_get_node(orig);
int err;
tsk = alloc_task_struct_node(node);
ti = alloc_thread_info_node(tsk, node);
}
进程关系
- parent:指向父进程
- children:所有子进程的组成的链表的链表头
- sibling:兄弟链表,又相当于父进程的 children链表中的一个节点
程序的运行需要使用到栈,所以不管进程是在内核态运行还是在用户态运行都需要用到栈 Linux将进程地址空间分为内核空间和用户空间,它们之间是不能直接访问的,而一个进程某些时候可能在用户态运行,某些时候可能在内核态运行(发生系统调用时),所以一个进程既需要用户栈又需要内核栈
在 task_struct 中,有一个变量指向该进程的内核栈,如下
struct task_struct {
...
void *stack;
...
};
内核栈的大小在内核中的定义如下
#define THREAD_SIZE_ORDER 1
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
一个 PAGE_SIZE 是4K,左移一位就是乘以2,所以 THREAD_SIZE 就是8K,所以大体结构如下 接下来我们看这8K的空间的结构分布 在这段空间的最底部,存放着一个 struct thread_info 结构体,何以证明呢?
在Linux中有一个 union thread_union 共用体,其定义如下
union thread_union {
#ifndef CONFIG_THREAD_INFO_IN_TASK
struct thread_info thread_info;
#endif
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
Linux中发生系统调用时,会从用户态变成内核态,然后执行内核代码,当内核代码执行完之后,又会回到用户态执行用户代码
在进程从用户态变成内核态的时候,内核需要将用户态运行时寄存器的值保存下来,然后修改寄存器,当内核代码执行完之后,又将寄存器的值恢复,这些寄存器的值保存在哪里呢?
在内核栈的最高端,存放着一个 pt_regs 结构,这个结构包含了相关寄存器的定义,用户态寄存器的值就保存在这里,对于X86 32 位其定义如下
struct pt_regs {
unsigned long bx;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
unsigned long bp;
unsigned long ax;
unsigned long ds;
unsigned long es;
unsigned long fs;
unsigned long gs;
unsigned long orig_ax;
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
};
此外剩余的空间才是用作函数栈,栈是向下生长的,所以进程的内核栈就变成下面这个样子 接下来看 thread_info 的定义,thread_info保存了特定于体系结构的汇编语言代码需要访问的那部分进程数据。如下
struct thread_info {
struct task_struct *task;
__u32 flags;
__u32 status;
__u32 cpu;
mm_segment_t addr_limit;
unsigned int sig_on_uaccess_error:1;
unsigned int uaccess_err:1;
};
Linux内核中可以通过 current 宏来获取当前正在运行的进程
#define get_current() (current_thread_info()->task)
#define current get_current()
current 通过 get_current(),进而调用 current_thread_info()->task
current_thread_info 的定义
static inline struct thread_info *current_thread_info(void)
{
return (struct thread_info *)
(current_stack_pointer & ~(THREAD_SIZE - 1));
}
current_stack_pointer 表示当前栈顶寄存器的值,对于 X86 就是 esp,在内核态的时候,current_stack_pointer 表示内核栈中的某一个位置
THREAD_SIZE 我们上面说过是8K,THREAD_SIZE - 1 就是8K剩下的所有位,如下
(current_stack_pointer & ~(THREAD_SIZE - 1)
意思就是将 current_stack_pointer 的低12位清空
我们从这个 current_thread_info 函数可以看出,通过这个操作就可以获得 thread_info 对象,这是为什么呢?
这是因为,内核栈在申请的时候,总是 8K 对齐的,也就是说地址的低12位肯定为0
当进程在内核态运行的时候,栈顶指针总是指向这块申请的内核栈中的某一个区域,内核栈的大小最大也就8K,所以将当前栈顶指针的低12位置零就可以得到内核栈的基址
而 thread_info 存在于内核栈的栈底处,所以也就获取到了该进程对应的 thread_info 结构
thread_info 结构中有一个 task_struct* 成员,指向该进程的 task_struct,所以也就可以获得该进程的 task_struct 结构
创造进程资源的copy
接下来会调用许多形如copy_xyz的例程,以便复制或共享特定的内核子系统的资源。task_struct包含了一些指针,指向具体数据结构的实例,描述了可共享或可复制的资源。由于子进程的task_struct是从父进程的task_struct精确复制而来,因此相关的指针最初都指向同样的资源,或者说同样的具体资源实例,如图2-10所示
假定我们有两个资源:res_abc和res_def。最初父子进程的task_struct中的对应指针都指向了资源的同一个实例,即内存中特定的数据结构。
如果CLONE_ABC置位,则两个进程会共享res_abc。此外,为防止与资源实例关联的内存空间释放过快,还需要对实例的引用计数器加1,只有进程不再使用内存时,才能释放。如果父进程或子进程修改了共享资源,则变化在两个进程中都可以看到。
如果CLONE_ABC没有置位,接下来会为子进程创建res_abc的一份副本,新副本的资源计数器初始化为1。 因此在这种情况下,如果父进程或子进程修改了资源,变化不会传播到另一个进程。
退出进程
进程必须用exit系统调用终止。这使得内核有机会将该进程使用的资源释放回系统 程序员可以显式调用exit。但编译器会在main函数(或特定语言使用的main函数)末尾自动添加相应的调用。
该调用的入口点是sys_exit函数,需要一个错误码作为其参数,以便退出进程。其定义是体系结构无关的,见 kernel/exit.c。它很快将工作委托给do_exit。 如果引用计数器归0而没有进程再使用对应的结构,那么将相应的内存区域返还给内存管理模块
|