热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

fork函数_fork和execve和Linux内核的一般执行过程

一、以fork和execve系统调用为例分析中断上下文的切换1.fork系统调用fork系统调用可以建立一个新进程,把当前的进程分为父进程和子进程,新进

一、以fork和execve系统调用为例分析中断上下文的切换

1.fork系统调用

fork系统调用可以建立一个新进程,把当前的进程分为父进程和子进程,新进程称为子进程,而原进程称为父进程。fork调用一次,返回两次,这两个返回分别带回它们各自的返回值,其中在父进程中的返回值是子进程的PID,而子进程中的返回值则返回 0。新创建的子进程与父进程十分类似,子进程得到与父进程用户级虚拟地址空间相同(但是独立)的一份拷贝,包括文本,数据和bss段、堆以及用户栈。子进程还获得与父进程任何打开文件描述符相同的拷贝。这就是意味着当父进程调用fork时候,子进程还可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大区别在于他们有着不同的PID。

先用具体调用例程分析fork:

#include
#include
#includeint main()
{FILE * fp = fopen("output.txt", "w");fputs("I am parentn", fp);switch(fork()){case -1:perror("fork failed");return -1;case 0:fputs("I am Childn", fp);break;default:break;}fclose(fp);return 0;
}

执行并查看输出的output.txt中的内容:

45143a025d6cdce17b53021d05436cbb.png

发现输出文件中会出现两行重复的parent文本,原因是 fputs 库函数带有缓冲,fork() 创建的子进程完全拷贝父进程用户空间内存时,fputs 库函数的缓冲区也被包含进来了。所以,fork() 执行之后,fork系统调用复制进程描述符及相关进程资源(采⽤写时复制技术)、分配⼦进程的内核堆栈并对内核堆栈和thread等进程关键上下⽂进⾏初始化,这样子进程获得了一份 fputs 缓冲区中的数据,导致“I am parent”这条消息在子进程中又被输出了一次。可见子进程确实继承了父进程大多数资源。

2.execve系统调用

execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。

一个execve系统调用例程:

#include
#include
#include
#include
#include int main() {int pid;pid = fork();if (pid <0){fprintf(stderr, "Fork Failedn");exit(-1);}else if (pid == 0){execlp("/bin/ls", "ls", NULL);printf("ls command run finishedn");}else{wait(NULL);printf("Child Completedn");exit(0);}return 0;
}

执行结果:

d48e409df1540e863f9cf49414fd3338.png

09b007408798c28ae9dadebf3cf7a3a1.png

可见只有子进程执行了ls命令,与上面的论述一致。

二、分析execve系统调用中断上下文的特殊之处

上面提到过,execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。exec簇函数都是通过execve系统调用进入内核,对应的系统调用内核处理函数为sys_execve或__x64_sys_execve,它们都是通过调用do_execve来具体执行加载可执行文件的工作,而do_execve又通过调用do_execve_common() 工作。查看do_execve_common() 代码可知execve整体调用流程大致如下:

  a. 陷入内核

  b. 加载新的进程

  c. 将新的进程,完全覆盖原先进程的数据空间

  d. 将 IP 值设置为新的进程的入口地址

  e. 返回用户态,新程序继续执行下去。老进程的上下文被完全替换,但进程的 pid 不变,所以 execve 系统调用不会返回原进程,而是返回新进程。

三、分析fork子进程启动执行时进程上下文的特殊之处

fork为56号系统调用,查看/syscall_64.tbl表得到内核函数__x64_sys_clone,由fork.c中的代码可知__x64_sys_clone调用的是do_fork函数。查看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;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);pid = get_task_pid(p, PIDTYPE_PID);nr = pid_vnr(pid);if (clone_flags & CLONE_PARENT_SETTID)put_user(nr, parent_tidptr);if (clone_flags & CLONE_VFORK) {p->vfork_dOne= &vfork;init_completion(&vfork);get_task_struct(p);}wake_up_new_task(p);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;
}

由do_fork源码可知fork子进程启动执行时调用了 copy_process 函数,复制当前进程产生子进程,并且传入关键参数为子进程设置响应进程上下文;还调用 wake_up_new_task 函数,将子进程放入调度队列中,从而有机会 CPU 调度并得以运行。 copy_process函数主要完成了调⽤dup_task_struct复制当前进程(⽗进程)描述符task_struct、信息检查、初始化、把进程状态设置为TASK_RUNNING(此时⼦进程置为就绪态)、采⽤写时复制技术逐⼀复制所有其他进程资源、调⽤copy_thread_tls初始化⼦进程内核栈、设置⼦进程pid等。其中最关键的就是dup_task_struct复制当前进程(⽗进程)描述符task_struct和copy_thread_tls初始化子进程内核栈。

四、以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程

系统调用是运行在内核态的,而用户程序一般是运行在用户态的,操作系统一般通过中断从用户态切换到内核态。中断具有两个属性一个是中断号,一个是中断向量表,是一个数组,包含中断处理程序。一个中断号对应一个中断处理程序。中断分为硬件中断和软件中断,软件中断通常是一条指令,带有一个参数代表中断号。

Linux系统的一般执行过程:在linux中使用int 0x80来触发所有的系统调用。和中断一样系统调用都有一个系统调用号,系统调用号代表在系统调用表中的位置。触发中断后,系统会自动保存当前中断上下文,然后进行中断上下文切换,之后执行中断处理程序,然后恢复现场,最后以sysret或iret返回系统调⽤。



推荐阅读
author-avatar
小HuLkfz_264
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有