基础知识

fork-exec 模式

Linux 系统中,所有的进程(包括 PID=1init 进程,但不包括 PID=0 的调度进程)的创建都遵循 fork-exec 模式。

  • fork :作为进程创建的第一阶段,其作用为将父进程的地址空间(代码段[只拷贝引用]、数据段、堆、栈)全部拷贝给子进程,父进程调用 fork 之后由内核进行操作,创建完之后给父进程返回创建好的子进程的 PID ,给子进程返回 0 (子进程的 task_struct 中会记录父进程的 PID )。
// 父进程与子进程共享代码段
#include <unistd.h>  // fork()的原型在这个头文件中

int pid = fork();
if (pid == -1 )  // 返回-1,说明fork失败  
{
    perror("fork");  
    exit(1);  
}
else if (pid > 0)  // 返回子进程pid,说明是父进程
{
    printf("parent PID = %d\n", getpid());
}
else if (pid == 0)  // 返回0,说明是子进程
{
    printf("child PID = %d, PPID = %d\n", getpid(), getppid());
}

[danger]危险:在 Linux 系统中,父进程执行 fork 只会复制发起调用的线程的信息,其他线程在子进程中会立即停止并消失,并且默认不会为这些线程调用清理函数以及针对线程局部存储变量的析构函数(可能引发死锁),关于这部分的详细内容会在第二章进行说明。

  • exec :作为进程创建过程中可选的第二阶段,如果我们需要在子进程空间里面运行全新的代码,可以使用 exec 函数族将新程序的代码段加载到子进程的地址空间。
else if (pid == 0)  //返回0,说明是子进程
{
    char  *argv[] = {"ls", "-al", "/etc/passwd", (char *)0};
    char  *envp[] = {"PATH=/bin", 0};
    // 只有execve是系统调用,其他exec函数都是它的包装
    execve("/bin/ls", argv, envp);
    exit(0);
}

[info]提示:由于写时拷贝(copy-on-write)技术的引入,在调用 fork 时,内核并不会立即拷贝整个父进程的地址空间给子进程(只读共享),而是在需要写入时才会拷贝,因此在 fork 之后立即调用 exec 就可以显著提升进程创建的效率 。

首先说一下fork和vfork的差别:fork 是 创建一个子进程,并把父进程的内存数据copy到子进程中。vfork是 创建一个子进程,并和父进程的内存数据share一起用。这两个的差别是,一个是copy,一个是share。你 man vfork 一下,你可以看到,vfork是这样的工作的,1)保证子进程先执行。2)当子进程调用exit()或exec()后,父进程往下执行。那么,为什么要干出一个vfork这个玩意? 原因是这样的—— 起初只有fork,但是很多程序在fork一个子进程后就exec一个外部程序,于是fork需要copy父进程的数据这个动作就变得毫无意了,而且还很重,所以,搞出了个父子进程共享的vfork。所以,vfork本就是为了exec而生。

就是fork后来采用的优化技术,这样,对于fork后并不是马上拷贝内存,而是只有你在需要改变的时候,才会从父进程中拷贝到子进程中,这样fork后立马执行exec的成本就非常小了。而vfork因为共享内存所以比较危险,所以,Linux的Man Page中并不鼓励使用vfork()

实际存在fork, clone, vfork 三个系统调用。fork是完全复制,clone则是有选择的复制,vfork则完全使用父进程的资源。

main函数并不是开始,之前有个函数(至少gcc是这样,别的不懂),姑且叫start,start函数里初始化一些我也不知道是什么的貌似并没有什么卵用的东西,然后调用main,return是函数的返回,main的return就返回到start,exit是退出进程,善后,然后自杀,参数是返回到调用进程的,比如shell,

每个C程序的入口点_start处的代码用伪代码表示为 _start:
call __libc_init_first // 一些初始化
call _init
call atexit
call main
call _exit 从伪代码就看出来了,每个C程序都要在执行一些初始化函数后对main调用,若main末尾为return语句,那么控制返回,最终会call _exit,把控制返回系统。若省略return,那么也将会call _exit。如果代码中有exit函数,那么会先执行atexit注册的函数,进而执行_exit()把控制还给操作系统。总之,这些情况下,当main返回,控制会传给系统

[success]技巧:在 Linux 2.6.32 版本之后提供了配置文件 /proc/sys/kernel/sched_child_runs_first ,将该文件的值置为非零可以让内核在创建完进程后会将子进程放在就绪态队列前端,避免父进程先写入数据导致无意义拷贝(但这并不能完全保证子进程优先执行)。

库函数 system

作为对 forkexecl 系统调用的封装, system 这个库函数是的代码执行部分等价于:

execl("/bin/sh", "sh", "-c", command, (char *) NULL);

实现简单的 shell

命令处理流程

命令预处理

为了避免创建无意义进程, bash 在执行命令前会按照以下步骤对输入的命令进行检查:

  • 检查命令是否包含 / ,如果不包含就检查当前 shell 的函数列表, 否则将命令视为外部命令执行。
  • 检查内置命令列表。
  • 检查 PATH 变量中的所有路径, bash 使用哈希表(内存中的数据存储区)记住可执行文件的完整路径名,因此可以避免多次重复的全局搜索。
  • 如果还是找不到就返回 127 状态码。
  • 如果命令不是以异步方式启动的,则 shell 将等待命令完成并获取其退出状态。

命令执行

命令执行环境

环境变量

参考资料

results matching ""

    No results matching ""