【Linux】进程等待与进程替换

版权声明:本文为博主原创文章,未经博主允许不得转载。Copyright (c) 2018, code farmer from sust. All rights reserved. https://blog.csdn.net/sustzc/article/details/82734231

进程的销毁
    1.释放资源
    2.记账信息
    3.将进程状态设置成僵尸状态
    4.转存储调度
进程终止的方法
    正常退出
        1.exit  (C库函数)  exit函数要先执行一些清除操作,然后才将控制权交给内核
            a.用户执行atexit或者onexit定义的清理函数;
            b.关闭所有的流,所有的缓存数据均被写入;
            c.调用_exit函数。
        2.main函数退出
        3._exit()  (操作系统提供的API,进程终止函数)  _exit函数执行后会立即返回给内核

    异常退出
        1.ctrl + c
        2.assert()
        3.abort()
        4.信号终止(ps:段错误、栈溢出)
        
"_NR"是在Linux的源码中为每个系统调用加上的前缀
    _exit终止调用进程,但不关闭文件,不清除输出缓存,也不调用出口函数。
    _exit()定义在unistd.h中,直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构。
    exit()函数定义在stdlib.h中,在这些基础上作了一些包装,在执行退出之前加了若干道工序.
    exit()函数与_exit()函数最大的区别就在于exit()函数在调用_exit系统调用之前要检查文件的打开情况,
    把文件缓冲区中的内容写回文件,就是”清理I/O缓冲”。  刷新缓存
    实际上,main函数执行完毕后,OS会注册一个清理函数并交给用户执行,其调用了atexit函数或者onexit函数
    atexit()回调函数 最多能注册32个,后注册先执行,类似于栈。
    打开一个文件就是打开一个流,在执行exit函数退出的时候,实际上会把文件的内容刷新到缓冲区,
    并且关闭流,最后执行_exit函数。

    尽量使用_exit函数
    
    return是一种常见的退出进程的方法,执行return n相当于exit(n),因为会把main函数的返回值当做exit函数的参数传入。
    
echo $?可以查看程序上一次退出的状态码  退出码的范围 0-255

进程等待:回收僵尸子进程

wait

pid_t wait(int *status);

    正常退出返回值为被回收的子进程ID
    错误返回-1。
    
8-15位是子进程的退出码

    一直等待子进程结束,才将返回结果反馈给wait。
    如果传递NULL,表示不关心子进程的退出状态信息。
    一次等待只能结束一个子进程。
    注意:wait必须和子进程的个数相匹配。(eg:接儿子上下学)

waitpid
pid_t waitpid(pid_t pid, int * status, int options)
status 子进程的退出码
举例:接指定的儿子(指定pid)上下学

参数pid
    >0   等待与其PID相等的子进程死亡
    =0   等待本组中的任何一个子进程死亡   fork创建的父子进程属于同一个组
    =-1  等待当前进程中的任何一个子进程死亡
    <-1  等待其组ID等于pid的绝对值|pid|的任一子进程死亡
status
    可以按照位图去理解,是个输出型参数,
        正常终止时,0-7位是0,8-15位表示退出的状态,即就是退出状态码。
        异常终止时,实际上就是被信号所终止,0-7位是终止信号 core dump标志,8-15位没有用。
option选项 一般写0  实际上也是个位图
    WNOHANG  
        非阻塞型等待
        轮询
        如果有子进程结束,则回收并返回子进程ID(ret > 0),如果此时没有子进程需要等待那么ret < 0
        如果子进程还没有结束,那么ret == 0

    WIFEXITED(status)
        如果正常退出,返回真
    WEXITSTATUS(status)  
        返回子进程的退出码
    WIFSIGNALED(status)
        如果被信号干掉,则返回真
    WTERMSIG(status)
        获得搞死该进程的信号值

头文件的访问顺序
    系统头文件->库函数头文件->自定义头文件

注意:只要PCB不被删掉,那么进程就一直存在。进程替换后,其原来进程后面的内容将不会被执行。

进程替换
    fork出子进程后,希望子进程运行和父进程(ps:比如运行a.out)不一样的进程(例如运行磁盘上的ls),这就需要进程替换了。

    一开始,虚拟内存通过三级映射(虚拟内存->页目录->页表)与物理内存建立连接,这时候如果想要运行磁盘上的ls进程,就需要调用
    exec系列函数,然后PCB不变,干掉虚拟内存通过三级映射与物理内存建立的连接,里面的代码段以及数据段都没有了,
    再将ls的代码段以及数据段放入物理内存的某个空间,然后再建立三级映射,
    通过readelf -h a.out可以看到程序执行的起始(入口)地址,这时候替换掉eip的值(改为ls的入口地址),那么就可以
    找到ls的入口地址,接下来运行ls这个进程,然后栈和堆需要重新开始(之前的函数栈帧并不能保证找到下个指令的开始地址),
    最终达到替换进程的目的。
    
    实际上,操作系统在创建PCB的同时,会创建一个加载器,加载器会解析文件格式,并将解析的结果存入物理内存中的代码段以及数据段中。
    
    加载器:也叫作程序加载器,主要用于加载程序和库,它负责将程序送入内存,为程序的运行提供准备,一旦加载完成,
    OS才会把控制权移交给执行代码的程序。
    Unix: loader(加载器)是系统调用(execve)的句柄。
    
    a.out是个ELF文件,通过readelf -h a.out可以看到程序执行的起始(入口)地址。可以看到这个字段Entry point address
    
exec系列函数
    下面这些函数如果调用成功,则加载新的程序从启动代码开始执行,不再返回,如果失败则返回-1,成功则没有返回值。
    如果执行exec系列函数后面还有别的语句,那么替换进程成功后,就不会有返回值。
        
list
    int execl(const char *path, const char *arg, ...);    //必须指明路径  
        execl("/bin/ls", "/bin/ls", "-l", "-t", NULL);
        execl("./hello", "./hello", NULL);
        execl("/bin/bash", "ps", "-ef", NULL);
    int execlp(const char *file, const char *arg, ...);
        execlp("ls", "ls", "-l", "-t", NULL);
    int execle(const char *path, const char *arg, ...,char *const envp[]);
        需要提前将envp传入指针数组中。
        execl("./hello", "./hello", NULL, envp);
            path绝对路径或者相对路径                   envp环境变量
            
vector      向量

char *argv[] = {"/bin/ls", "-l", NULL};    

    int execv(const char *path, char *const argv[]);
        execv("/bin/ls", argv);
    int execvp(const char *file, char *const argv[]);
        execvp("ls", argv);    //实际上exec系列函数只是将参数传进去,并不会进行校验,因此直接替换掉argv[0]的值。
    int execve(const char *path, char *const argv[], char *const envp[]);      //系统调用
        execve("./h", argv, env);
            file可执行文件名   argv(main函数的命令行参数)
    其余5个都是C库函数
    
l(list)    可变参数列表
v(vector)    数组
p(path)    自动搜索已存在的环境变量
e(env)    需要自己维护的环境变量

模拟system

    /bin/sh -c "ls -l"
    
    if (0 == pid)
    {
        char *argv[] = {
             "sh", "-c", cmd, NULL
        };

        execvp("/bin/sh", argv);
        exit(127);
    }
    else
    {
        int status ;
 
        waitpid(pid, &status, 0);
        
        if ( WIFEXITED(status) )
        {
             ret = WEXITSTATUS(status);
        }
        else
        {
             ret = -1;
        }
    }


find / -name ls 2>/dev/null   查找ls的位置,并且过滤掉打印的错误信息
    这里2表示strerr,也就是文件描述符中的标准错误对应的返回值,/dev/null是linux中的一个特殊文件,相当于一个垃圾桶。
    它丢弃一切写入其中的数据(但报告写入操作成功),读取它则会立即得到一个EOF。通常被用于丢弃不需要的数据输出。
    
清空文件内容:
    1.  echo "" > test.txt  文件大小被截断成1个字节 echo > test.txt
    2.  > test.txt
    3.  cat /dev/null > test.txt
    4.  cp /dev/null test.txt
    5.  dd if=/dev/null of=test.txt
    6.  truncate -s 0 test.txt      truncate用来将一个文件缩小或者扩展到给定大小   -s来指定文件大小
    
    /dev/zero实际上产生连续不断的null的流(二进制的零流,而不是ASCII型的)。写入它的输出会丢失不见,
/dev/zero主要的用处是用来创建一个指定长度用于初始化的空文件,像临时交换文件。
为特定的目的而用零去填充一个指定大小的文件, 如挂载一个文件系统到环回设备。
该设备无穷尽地提供0,可以使用任何你需要的数目——设备提供的要多的多。他可以用于向设备或文件写入字符串0。
bash
    首先bash可以看成是一个父进程,那么输入ls命令时,实际上是从磁盘上获取到ls的代码加载到物理内存上,然后再运行ls进程,
这也就是进程替换,ls进程运行结束后,被bash进程回收,(实际上bash一直在等待ls进程执行结束)。

简易版的shell
    1.从标准输入中读取字符串到内存中;
    2.对用户输入进行解析,要执行的指令是什么,参数是什么;
        参数开始
                当前状态为参数结束状态,并且当前字符为非空格,此时进入参数开始状态,并且把当前指针保存在一个数组中。
        参数结束
                当前状态为参数开始状态,并且当前字符为空格,此时进入参数结束状态,并且把当前指针指向的字符改为'\0'。
        strtok
        ll是个别名
    3.创建子进程fork
        a.子进程进行父进程的程序替换execvp;
        b.父进程进行进程等待wait。
    4.当子进程执行完毕后从wait中返回,继续下一次循环。
关于myshell中cd不生效:
    执行cd ..时,myshell又会创建一个子进程,实际上子进程对cd进行了程序替换,
但是由于子进程执行结束后替换的目录就被销毁了,然而myshell的目录仍是之前的目录,
因此看不到切换目录。
    如果发现输入的指令是cd,直接调用chdir函数修改进程自身的工作目录。

1.普通命令:shell创建子进程进行程序替换来实现;
2.内建指令:shell进程自身判定输入的指令后,调用相关系统函数实现,本质上是对shell进程自身来操作。    

myshell不支持
    1.alias
    2.内建指令(比如cd)
    3.管道
    4.输出重定向
    5.命令提示符中不能显示用户名,主机名,当前目录

猜你喜欢

转载自blog.csdn.net/sustzc/article/details/82734231