Linux内核创建一个新进程
张雨梅 原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-10000
创建新进程
如果同一个程序被多个用户同时运行,那么这个程序就有多个相对独立的进程,与此同时他们又共享相同的执行代码。在Linux系统中进程的概念类似于任务或者线程
创建进程,调用fork函数,这是一个系统函数。fork函数与系统函数的调用大体相同,但是fork之后产生了一个新的进程,会发生两次返回。
task_struct的数据结构
每个可以独立被调度的执行上下文都有一个进程描述符,不管是进程还是线程都有自己的 task_struct结构
task_struct的定义链接是http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#task_struct,其代码很长,下面就列出一部分
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped 描述进程运行状态*/
void *stack; //指定进程的内核堆栈
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace; #ifdef CONFIG_SMP
struct llist_node wake_entry;
int on_cpu;
struct task_struct *last_wakee;
unsigned long wakee_flips;
unsigned long wakee_flip_decay_ts; int wake_cpu;
#endif
int on_rq; //运行队列 int prio, static_prio, normal_prio; //定义优先级
unsigned int rt_priority;
const struct sched_class *sched_class; //进程调度相关的定义
struct sched_entity se;
struct sched_rt_entity rt;
#ifdef CONFIG_CGROUP_SCHED
struct task_group *sched_task_group;
#endif
struct sched_dl_entity dl;
......
linux进程的状态有
task_running,就绪态、运行态都用这个表示,这与操作系统中的定义不同,这种定义方式表示进程是可运行的,就绪状态不同的是要等待cpu的调度。
task_zombie,僵尸状态,即死锁
task_interruptible或task_uninterruptible,阻塞状态
还有_task_stopped,_task_traced,exit_zombie,exit_dead等各种状态。
PCB中指定了内核栈,没有指定用户栈,这是因为进程由用户态进入内核态时,保存了用户态进程的现场,恢复现场时,即可找到用户栈。
struct list_head tasks;//进程链表,链接所有当前的进程
#ifdef CONFIG_SMP
struct plist_node pushable_tasks;
struct rb_node pushable_dl_tasks;
#endif
其中list_head是一个双向链表,表示当前进程的前驱和后继关系,其定义是
struct list_head{
struct list_head *next,*prev;
};
struct mm_struct *mm, *active_mm;
这是进程的地址空间管理,与数据段、代码段、分页、分段、物理空间用户逻辑空间的转换有关,这里忽略掉这些细节。Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_info和进程的内核堆栈。
pid_t pid;
pid_t tgid;
pid,process id
tgid,thread group id
linux给每一个进程和轻量级进程都分配一个pid,程序员希望向进程发送信号时,此信号可以影响进程及进程产生的轻量级进程,因此产生了线程组(可以理解为轻量级进程组)的概念,在线程组内,每个线程都使用此线程组内第一个线程的pid,并将此值存入tgid。也就是说一个组内的线程的tgid相同。
#ifdef CONFIG_CC_STACKPROTECTOR
/* Canary value for the -fstack-protector gcc feature */
unsigned long stack_canary;
#endif
stack-canary是设置的栈警卫,可以用来保护栈防止被攻击,它所在的地址如果被改写,就认为栈受到了攻击。
struct task_struct __rcu *real_parent; /* real parent process */
struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
/*
1345 * children/sibling forms the list of my natural children
1346 */
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
struct task_struct *group_leader; /* threadgroup leader */
......
这一段定义了进程的父子关系,组等,list-head是进程链表,含有前驱、后继的设置,用来表示进程的父子关系,ptraced做调试使用, 前面介绍的tgid的值就是*group_leader的进程号。
cputime_t utime, stime, utimescaled, stimescaled;
cputime_t gtime;
#ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE
struct cputime prev_cputime;
#endif
......//和时间相关的一些定义
/* CPU-specific state of this task */
struct thread_struct thread;
这个是和cpu相关的状态,其中包括sp、ip、es、fs、gs、error_code等。是一个比较重要的数据结构。其他定义这里先不分析。
fork函数对应的内核处理过程sys_clone
fork函数用于创建子进程,创建的进程与当前的进程是一个复制的过程,但是中间也会有修改,比如进程的id等。创建子进程之后,子进程与父进程可以说是独立的,可以各自运行。创建进程要发生系统调用,调用system_call,执行相应的系统函数,这里就是sys_clone。fork()调用执行一次返回两个值,对于父进程,fork函数返回子程序的进程号,而对于子程序,fork函数则返回零,这就是fork函数的一次调用,两次返回。
int main(int argc, char * argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid < )
{
/* error occurred */
fprintf(stderr,"Fork Failed!");
exit(-);
}
else if (pid == )
{
/* child process */
printf("This is Child Process!\n");
}
else
{
/* parent process */
printf("This is Parent Process!\n");
/* parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!\n");
}
}
比如上面的程序,pid=0,表示子进程,pid>0表示父进程,在子进程中,执行printf("This is Child Process!\n");在父进程中,执行printf("This is Parent Process!\n");也就是说这里的if,else两种情况都会执行到。
fork创建的子进程,从用户态的角度看,是从fork的下一句命令执行。在内核中,fork是一个新创建的进程,从my_process开始执行。
linux中进程创建有三种方法fork,vfork,clone。
fork,子进程只是完全复制父进程的资源,子进程有自己的task_struct结构和pid, 数据空间,堆和栈的副本都是复制父进程的。
vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。用 vfork创建子进程后,父进程会被阻塞直到子进程调用exec(exec,将一个新的可执行文件载入到地址空间并执行之)或exit。vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的 ,因此通过vfork共享内存可以减少不必要的开销。
clone()带有参数,fork()和vfork()是无参数的,fork()是全部复制,vfork()是共享内存,clone()是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的clone_flags来决定。另外,clone()返回的是子进程的pid。
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, , , NULL, NULL);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, ,
, NULL, NULL);
}
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#endif
{
return do_fork(clone_flags, newsp, , parent_tidptr, child_tidptr);
}
#endif
可以看到fork,vfork,clone函数都会调用do_fork函数。clone函数有些参数,linux定义了多种参数不同的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;
int trace = ;
long nr;
......
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
do_fork实现进程的创建,调用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; if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))//出错处理
return ERR_PTR(-EINVAL);
......
retval = security_task_create(clone_flags);
if (retval)
goto fork_out; retval = -ENOMEM;
p = dup_task_struct(current);//复制进程的pcb
if (!p)
goto fork_out; ftrace_graph_init_task(p); rt_mutex_init_task(p);
1247
......
p->utime = p->stime = p->gtime = 0;//修改子进程的参数,也是初始化
p->utimescaled = p->stimescaled = 0;
#ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE
p->prev_cputime.utime = p->prev_cputime.stime = 0;
#endif
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
seqlock_init(&p->vtime_seqlock);
p->vtime_snap = 0;
p->vtime_snap_whence = VTIME_SLEEPING;
#endif
copy_process是创建进程的主要代码。前面定义了一些出错处理实现进程的复制,同时修改子进程中的一些信息,pid等。其中有copy_thread,包含thread的sp、ip等。然后调用dup_task_struct函数。
305static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;//
struct thread_info *ti;
int node = tsk_fork_get_node(orig);
int err; tsk = alloc_task_struct_node(node);//分配一个结点空间
if (!tsk)
return NULL; ti = alloc_thread_info_node(tsk, node);//分配一个进程内核堆栈空间,创建了一个页面,一部分存放thread_info,一部分存放内核堆栈(从高地址向低地址)
if (!ti)
goto free_tsk; err = arch_dup_task_struct(tsk, orig);//复制orig进程的信息给当前进程
if (err)
goto free_ti;
复制进程的时候会调用arch_dup_task_struct,功能是把一个指针赋给另一个指针,实现的股票功能就是把orig赋给tsk。
int __weak arch_dup_task_struct(struct task_struct *dst,
struct task_struct *src)
{
*dst = *src;
return ;
}
然后执行dup_task_struct函数,复制父进程的内核堆栈,copy_thread函数,拷贝寄存器信息,esp,eip等,然后到ret_from_fork,是子进程在内核中执行的起点。
首先修改实验楼的menuos操作系统,添加fork系统函数。在实验楼的虚拟机上进行实验,在终端输入命令,可以看到menuo已经包含fork指令。
用gdb调试观察fork的执行过程,在终端输入命令:
qemu -kernel linux3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
重新打开一个终端,用gdb调试
gdb
file linux-3.18.6/vmlinux
target remote:1234
设置断点为:sys_clone, do _fork, copy_process, dup_task_struct, copy_thread, ret_from_fork
在本次实验的menuos环境中,fork, vfork, clone调用的是sys_clone,不是sys_fork, 然后都会调用do_fork.
观察到fork一个子进程时,其执行过程是
总结
进程控制块PCB,包含了进程中的各种信息,pid,状态,内核栈,进程链表,调度相关的定义,内存管理,文件系统等,内容很强大很复杂。fork产生一个子进程,与父进程的情况大概相同,之后子进程与父进程是相互独立的。在父进程中的返回和一般系统调用一样,在子进程返回到ret_from_fork。
参考资料:http://blog.sina.com.cn/s/blog_7673d4a5010103x7.html fork,vfork,clone
Linux内核创建一个新进程的更多相关文章
- linux内核分析作业6:分析Linux内核创建一个新进程的过程
task_struct结构: struct task_struct { volatile long state;进程状态 void *stack; 堆栈 pid_t pid; 进程标识符 u ...
- 第六周分析Linux内核创建一个新进程的过程
潘恒 原创作品转载请注明出处<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 task_struct结构: ...
- 实验 六:分析linux内核创建一个新进程的过程
实验六:分析Linux内核创建一个新进程的过程 作者:王朝宪 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029 ...
- 20135202闫佳歆--week6 分析Linux内核创建一个新进程的过程——实验及总结
week 6 实验:分析Linux内核创建一个新进程的过程 1.使用gdb跟踪创建新进程的过程 准备工作: rm menu -rf git clone https://github.com/mengn ...
- 《Linux内核--分析Linux内核创建一个新进程的过程 》 20135311傅冬菁
20135311傅冬菁 分析Linux内核创建一个新进程的过程 一.学习内容 进程控制块——PCB task_struct数据结构 PCB task_struct中包含: 进程状态.进程打开的文件. ...
- 作业六:分析Linux内核创建一个新进程的过程
分析Linux内核创建一个新进程的过程 进程描述符PCB----task_struct数据结构 操作系统:1.进程管理 2.内存管理 3 文件系统 一.新进程如何创建和修改task_struct数据结 ...
- Linux内核分析-分析Linux内核创建一个新进程的过程
作者:江军 ID:fuchen1994 实验题目:分析Linux内核创建一个新进程的过程 阅读理解task_struct数据结构http://codelab.shiyanlou.com/xref/li ...
- Linux内核分析第六周学习笔记——分析Linux内核创建一个新进程的过程
Linux内核分析第六周学习笔记--分析Linux内核创建一个新进程的过程 zl + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/U ...
- 第六周——分析Linux内核创建一个新进程的过程
"万子恵 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 &q ...
- 实验六:分析Linux内核创建一个新进程的过程
原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 题目自拟,内容围绕对Linu ...
随机推荐
- UEFI引导在GPT分区下安装win2008——抓住那只傲娇的win2008
上周遇到个客户DELL R520的服务器新采购了8块3T硬盘做备份服务器,raid配置5+1,一个磁21.8T.先用普通的装desktop OS的方法发现进去没raid盘,然后就按照官方的文档进入Li ...
- eclipse tomcat 集成
1. 下载 Tomcat 作者选择的是 Tomcat6,下载地址:http://tomcat.apache.org/download-60.cgi,选择绿色版的 zip 进行下载(目前最 ...
- The connection to adb is down,and a server error has occured.解决办法---------------------亲测有效
认真读error: 办法一: 点击项目右键->Android tools ->Fix Project Properties,检查项目属性 办法二: 设备管理器,查看是否存在adb进程 如果 ...
- Linux系统安装-系统分区
ctrl+alt+enter全屏
- C#中try catch中throw ex和throw方式抛出异常有何不同
我们在C#的try catch代码块中里面经常使用throw语句抛出捕捉到的异常,但是你知道吗使用throw ex和throw抛出捕获到的异常效果是不一样的. 异常捕捉的原理 首先先介绍一下C#异常捕 ...
- 使用java mail 发送邮件
1.关联jar包: activation.jar mail.jar 2.调用 @Test public void test1() { List<String> imageUrlLi ...
- Base:-用数组赋值实现while和shift功能
3列数组,arrayA,arrayB,arrayC:分别一一对应主机及其主机所能创建的资源数,还有arrayC表示需要创建的资源数:arrayA=("192.168.1.1" &q ...
- 20145227&20145201 《信息安全系统设计基础》实验四
北京电子科技学院(BESTI) 实 验 报 告 课程:信息安全系统设计基础 班级:1452 姓名:(按贡献大小排名)鄢曼君 李子璇 学号:(按贡献大小排名)20145227 20145201 成绩: ...
- 逗比的wifi开关
笔记本会出现网卡开机不能用的现象.具体表现为:网卡没有禁用,但是搜索不到无线信号.适配器选项框里面选中无线网卡,然后诊断这个链接提示启用无线功能.然后我点击应用此修复就能搜索到无线信号了.问题是,电脑 ...
- 笔记汇总.md
```javascript 1.js对象,value的值取不到,试着将引入的js文件放到body的后面 $("input[type='time']").change( functi ...