第4章 进程的一生

    典型的进程的生命周期:

▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁      

        
【4.1】进程ID
    1.Linux 下每个进程都会有一个非负整数表示的唯一进程 ID                
    2.Linux 提供了 getpid 函数来获取进程的 pid ,同时还提供了 getppid 函数来获取父进程的 pid ,                
      

 pid_t getpid(void);
 pid_t getppid(void);

    3.每个进程都有自己的父进程,父进程又会有自己的父进程,最终都会追溯到 1 号进程即 init 进程
    4.可以通过 pstree 的命令来查看进程的家族树。
    5.procfs 文件系统会在 /proc 下为每个进程创建一个目录,名字是该进程的 pid 。
    6.虽然进程 ID 是唯一的,但是进程 ID 可以重用。    

▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁                    
【4.2】进程的层次
①层次
    1.每个进程都有父进程,父进程也有父进程,这就形成了一个以 init 进程为根的家族树。除此以外,进程还有其他层次关系:进程、进程组和会话。
    2.进程组:是一组相关进程的集合
    3.会话是一组相关进程组的集合
    // 用人来打比方,会话如同一个公司,进程组如同公司里的部门,进程则如同部门里的员工。
    4.进程会有如下 ID:
        ·PID :进程的唯一标识。对于多线程的进程而言,所有线程调用 getpid 函数会返回相同的值。
        ·PGID :进程组 ID 。每个进程都会有进程组 ID ,表示该进程所属的进程组。默认情况下新创建的进程会继承父进程的进程组 ID 。
        ·SID :会话 ID 。每个进程也都有会话 ID 。默认情况下,新创建的进程会继承父进程的会话 ID 。  

    5.查看所有进程的层次关系:
        ps -ejH
        ps axjf    

    6.获取其进程组 ID 和会话 ID 
        pid_t getpgrp(void);
        pid_t getsid(pid_t pid);    

    7.进程组和会话是为了支持 shell 作业控制而引入的概念。
        1.当有新的用户登录 Linux 时,登录进程会为这个用户创建一个会话。用户的登录 shell 就是会话的首进程。会话的首进程 ID 会作为整个会话的 ID 。会话是一个或多个进程组的集合,囊括了登录用户的所有活动。
        2.在登录 shell 时,用户可能会使用管道,让多个进程互相配合完成一项工作,这一组进程属于同一个进程组。
        3.当用户通过 SSH 客户端工具( putty 、 xshell 等)连入 Linux 时,与上述登录的情景是类似的。


②进程组
    1.修改进程组 ID 的接口
        int setpgid(pid_t pid, pid_t pgid);

    2.会话
        1.会话是一个或多个进程组的集合,以用户登录系统为例,可能存在如图 4-3 所示的情况
            会话首进程            后台进程组            前台进程组
            登录shell            proc1→proc2        proc5→proc6
                                →proc3→proc4        
        2.setsid 函数来创建会话
            pid_t setsid(void);
            1)创建一个新会话,会话 ID 等于进程 ID ,调用进程成为会话的首进程。
            2)创建一个进程组,进程组 ID 等于进程 ID ,调用进程成为进程组的组长。
            3)该进程没有控制终端,如果调用 setsid 前,该进程有控制终端,这种联系就会断掉。
            调用 setsid 函数的进程不能是进程组的组长,否则调用会失败,返回 -1 ,并置 errno 为 EPERM 。


▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁                    
【4.3】进程的创建之 fork()
    pid_t fork(void);
        1.进程可以调用 fork 函数来创建新的进程。调用进程为父进程,被创建的进程为子进程。
        2.与普通函数不同, fork 函数会返回两次。 fork 函数向子进程返回 0 ,并将子进程的进程 ID 返给父进程。当然了,如果 fork 失败,该函数则返回 -1 ,并设置 errno 。
        3.注意:POSIX 标准和 Linux 都没有保证会优先调度父进程。因此在应用中,决不能对父子进程的执行顺序做任何的假设。如果确实需要某一特定执行的顺序,那么需要使用进程间同步的手段。 

①fork 之后父子进程的内存关系
    1.fork 之后的子进程完全拷贝了父进程的地址空间,包括栈、堆、代码段等。 // 如全局变量, 局部变量, malloc分配的变量
    2.如果父子进程对相应的数据进行修改,那么两个进程是并行不悖、互不影响的。
    3. fork 时子进程完全拷贝父进程的数据段、栈和堆的做法是不明智的,因为接下来的子进程可能指向 exec 系列函数会毫不留情地抛弃刚刚辛苦拷贝的内存。
        ==> Linux 引入了写时拷贝( copy-on-write )的技术
    4.写时拷贝技术:
        写时拷贝是指子进程的页表项指向与父进程相同的物理内存页,这样只拷贝父进程的页表项就可以了,当然要把这些页面标记成
        只读(如图 4-4 所示)。如果父子进程都不修改内存的内容,大家便相安无事,共用一份物理内存页。但是一旦父子进程中有任何一方尝试修改,就会引发缺页异常( page fault )。此时,内核会尝试为该页面创建一个新的物理页面,并将内容真正地复制到新的物理页面中,让父子进程真正地各自拥有自己的物理内存页,然后将页表中相应的表项标记为可写。
        //说明: 对于没有修改的页面,内核并没有真正地复制物理内存页,仅仅是复制了父进程的页表。这种机制的引入提升了 fork 的性能,从而使内核可以快速地创建一个新的进程。

②fork 之后父子进程的内存关系
    1.执行 fork 函数,内核会复制父进程所有的文件描述符。对于父进程打开的所有文件,子进程也是可以操作的。父子进程同时操作同一个文件是互相影响的。

    2.无论父进程还是子进程调用 read 函数导致文件偏移量后移都会被对方获知,这表明父子进程共用了一套文件偏移量。

    3.对于fork的子进程,在执行exec函数时没有关闭子进程的文件描述符, 任可操作, 这带来了权限安全问题 Linux 引入了 close on exec 机制。设置了 FD_CLOSEXEC 标志位的文件,在子进程调用 exec 家族函数时会将相应的文件关闭。

    4.设置了 FD_CLOSEXEC 标志位的方法:
        ·open 时,带上 O_CLOSEXEC 标志位。    // 推荐
        ·open 时如果未设置,那就在后面调用 fcntl 函数的 F_SETFD 操作来设置
        
③fork 之后父子进程的内存关系
    1.在内核的进程描述符 task_struct 结构体中,与打开文件相关的变量如下所示:

 struct task_struct {
    ...struct files_struct *files;...
 }


    2.不难看出,父子进程之间拷贝的是 struct file 的指针,而不是 struct file 的实例,父子进程的 struct file 类型指针,都指向同一个 struct file 实例。 

    3. struct file 成员变量
      

  struct file{
      loff_t f_pos; ...  /* 文件位置指针的当前值,即文件偏移量 */
  }

        ==》因为父子进程的指针都指向了同一个 struct file 结构体。所以父子进程是如何共享文件偏移量。


▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁                    
【4.4】进程的创建之 vfork()
    在早期的实现中, fork 没有实现写时拷贝机制,而是直接对父进程的数据段、堆和栈进行完全拷贝,效率十分低下。很多程序在 fork 一个子进程后,会紧接着执行 exec 家族函数,这更是一种浪费。所以 BSD 引入了 vfork 。既然 fork 之后会执行 exec 函数,拷贝父进程的内存数据就变成了一种无意义的行为,所以引入的 vfork 压根就不会拷贝父进程的内存数据,而是直接共享。再后来 Linux 引入了写时拷贝的机制,其效率提高了很多,这样一来, vfork 其实就可以退出历史舞台了。除了一些需要将性能优化到极致的场景,大部分情况下不需要再使用 vfork 函数了。

    1.vfork 会创建一个子进程,该子进程会共享父进程的内存数据,而且系统将保证子进程先于父进程获得调度。
    2.子进程也会共享父进程的地址空间,而父进程将被一直挂起,直到子进程退出或执行 exec 。
    3.vfork 之后,子进程如果返回,则不要调用 return ,而应该使用 _exit 函数。如果使用 return 返回,就意味着 main 函数返回了,因为栈是父子进程共享的,所以程序的函数栈发生了变化。
    4.一般来说, vfork 创建的子进程会执行 exec ,执行完 exec 后应该调用 _exit 返回。注意是 _exit 而不是exit 。因为 exit 会导致父进程 stdio 缓冲区的冲刷和关闭。


▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁                    
【4.5】daemon 进程的创建
    1.daemon 进程又被称为守护进程,一般来说它有以下两个特点:
        1.生命周期很长,一旦启动,正常情况下不会终止,一直运行到系统退出。
        2. 在后台执行,并且不与任何控制终端相关联。
    2.创建一个 daemon 进程的步骤被概括地称为 double-fork magic 。
        (1)执行 fork ()函数,父进程退出,子进程继续
        (2)子进程执行如下三个步骤,以摆脱与环境的关系
            1)修改进程的当前目录为根目录( / )。
            2)调用 setsid 函数。这个函数的目的是切断与控制终端的所有关系,并且创建一个新的会话。
            3)设置文件模式创建掩码为 0 。        
        (3)再次执行 fork ,父进程退出,子进程继续        
        (4)关闭标准输入( stdin )、标准输出( stdout )和标准错误( stderr )        
    3.glibc 提供了 daemon 函数,从而帮我们将程序转化成 daemon 进程。
        int daemon(int nochdir, int noclose);
            其中的 nochdir ,用来控制是否将当前工作目录切换到根目录。
            ·0 :将当前工作目录切换到 / 。
            ·1 :保持当前工作目录不变。        
            而 noclose ,用来控制是否将标准输入、标准输出和标准错误重定向到 /dev/null 。
            ·0 :将标准输入、标准输出和标准错误重定向到 /dev/null 。
            ·1 :保持标准输入、标准输出和标准错误不变。        
            一般情况下,这两个入参都要为 0 。    //ret = daemon(0,0)  

 
▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁                    
【4.6】进程的终止
    在不考虑线程的情况下,进程的退出有以下 5 种方式。
    正常退出有 3 种:
        · 从 main 函数 return 返回
        · 调用 exit
        · 调用 _exit
    异常退出有两种:
        · 调用 abort
        · 接收到信号,由信号终止


①_exit 函数
    void _exit(int status);
        1. status 参数定义了进程的终止状态,父进程可以通过 wait ()来获取该状态值。需要注意的是返回值,虽然 status 是 int 型,但是仅有低 8 位可以被父进程所用。所以写 exit ( -1 )结束进程时,在终端执行 “$ ? ” 会发现返回值是 255 。

②exit 函数
    (1)void exit(int status);
       exit()函数的最后也会调用 _exit ()函数,但是 exit 在调用 _exit 之前,还做了其他工作:    
        1)执行用户通过调用 atexit 函数或 on_exit 定义的清理函数。
        2)关闭所有打开的流( stream ),所有缓冲的数据均被写入( flush ),通过 tmpfile 创建的临时文件都会被删除。
        3)调用 _exit 。    
    (2)exit 函数和 _exit 函数的不同之处:
        1.首先是 exit 函数会执行用户注册的清理函数。用户可以通过调用 atexit ()函数或 on_exit ()函数来定义清理函数。这些清理函数在调用 return 或调用 exit 时会被执行。执行顺序与函数注册的顺序相反。当进程收到致命信号而退出时,注册的清理函数不会被执行;当进程调用 _exit 退出时,注册的清理函数不会被执行;当执行到某个清理函数时,若收到致命信号或清理函数调用了 _exit ()函数,那么该清理函数不会返回,从而导致排在后面的需要执行的清理函数都会被丢弃。

        2.其次是 exit 函数会冲刷( flush )标准 I/O 库的缓冲并关闭流。 glibc 提供的很多与 I/O 相关的函数都提供了缓冲区,用于缓存大块数据。
          缓冲有三种方式:无缓冲( _IONBF )、行缓冲( _IOLBF )和全缓冲( _IOFBF )。
            · 无缓冲:就是没有缓冲区,每次调用 stdio 库函数都会立刻调用 read/write 系统调用。
            · 行缓冲:对于输出流,收到换行符之前,一律缓冲数据,除非缓冲区满了。对于输入流,每次读取一行数据。
            · 全缓冲:就是缓冲区满之前,不会调用 read/write 系统调用来进行读写操作。
          如果不冲刷缓冲区,缓冲区的数据就会丢失。exit 函数在关闭流之前,会冲刷缓冲区的数据,确保缓冲区里的数据不会丢失。

        3.存在临时文件, exit 函数会负责将临时文件删除
        4.exit 函数的最后调用了 _exit ()函数,最终殊途同归,走向内核清理。
    
③return 退出
    return 是一种更常见的终止进程的方法。执行 return( n )等同于执行 exit( n ),因为调用 main()的运行时函数会将 main 的返回值当作 exit 的参数。

▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁                    
【4.7】等待子进程

①僵尸进程
    (1)说明
        1.进程退出时会进行内核清理,基本就是释放进程所有的资源,这些资源包括内存资源、文件资源、信号量资源、共享内存资源,或者引用计数减一,或者彻底释放。不过,进程的退出其实并没有将所有的资源完全释放,仍保留了少量的资源,比如进程的 PID 依然被占用着,不可被系统分配。此时的进程不可运行,事实上也没有地址空间让其运行,进程进入僵尸状态。

        2.僵尸进程依然保留的资源有进程控制块 task_struct 、内核栈等。这些资源不释放是为了提供一些重要的信息,比如进程为何退出,
 是收到信号退出还是正常退出,进程退出码是多少,进程一共消耗了多少系统 CPU 时间,多少用户 CPU 时间,收到了多少信号, 发生了多少次上下文切换,最大内存驻留集是多少,产生多少缺页中断?等等。这些信息,就像墓志铭,总结了进程的一生。

        3.一旦父进程收集了这些信息之后(通过调用下面提到的wait/waitpid 等函数),这些残存的资源完成了它的使命,就可以释放了,进程就脱离僵尸状态,彻底消失了。

    (2)查看僵尸进程
        1.ps 命令输出的进程状态 Z ,就表示进程处于僵尸状态,
        2.procfs 提供的 status 信息中的 State 给出的值是 Z ( zombie ),也表明进程处于僵尸状态。

    (3)清理僵尸进程
       进程一旦进入僵尸状态,就进入了一种刀枪不入的状态,“ 杀人不眨眼 ” 的 kill-9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
       清除僵尸进程有以下两种方法:
        · 父进程调用 wait 函数,为子进程 “ 收尸 ” 。
        · 父进程退出, init 进程会为子进程 “ 收尸 ” 。

    (4)防范僵尸进程的操作
        1.应该将父进程对 SIGCHLD 的处理函数设置为 SIG_IGN 。        
        2.或者在调用 sigaction 函数时设置 SA_NOCLDWAIT 标志位。
         说明:
            1.这两者都会明确告诉子进程,不会为子进程 “ 收尸 ” 。
            2.子进程退出的时候,内核会检查父进程的 SIGCHLD 信号处理结构体是否设置了 SA_NOCLDWAIT 标志位,
              或者是否将信号处理函数显式地设为 SIG_IGN 。
            3.如果显示的设置了SIGCHLD为SIG_IGN, 或者SIGCHLD带SA_NOCLDWAIT标志, 那么子进程退出时会"自行了断",
              不会变为僵尸进程。

②等待子进程之wait()
    pid_t wait(int *status);
        成功时,返回已退出子进程的进程 ID ;失败时,则返回 -1 并设置 errno ,
    (1)说明:
        1.wait()函数等待的是任意一个子进程,任何一个子进程退出,都可以让其返回。
        2.如果没有子进程处于僵尸状态,wait()的调用进程会陷入阻塞。 
        3.当多个子进程都处于僵尸状态, wait()函数获取到其中一个子进程的信息后立刻返回(不遵循编程僵尸进程时间先后顺序)。
        4.由于 wait ()函数不会接受 pid_t 类型的入参,所以它无法明确地等待特定的子进程。

    (2)wait()函数返回有三种可能性:
        ·等到了子进程退出,获取其退出信息,返回子进程的进程 ID 。
        ·等待过程中,收到了信号,信号打断了系统调用,并且注册信号处理函数时并没有设置 SA_RESTART 标志位,系统调用不会被重 启, wait ()函数返回 -1 ,并且将 errno 设置为 EINTR 。
        ·已经成功地等待了所有子进程,没有子进程的退出信息需要接收,在这种情况下, wait()函数返回 -1 , errno 为 ECHILD 。  

       (3)wait()的局限性
        1.不能等待特定的子进程。
        2.如果不存在子进程退出, wait()只能阻塞。
        3.wait()函数只能发现子进程的终止事件,不能发现信号停止和信号继续事件
        // 由于上述三个缺点的存在,所以 Linux 又引入了 waitpid ()函数。

    (4)用宏处理status, 来判断子进程退出时的状态, 请参考wait_pid
    
③等待子进程之waitpid()
    pid_t waitpid(pid_t pid, int *status, int options);    
       pid:        
        ·pid > 0 :表示等待进程 ID 为 pid 的子进程,也就是上文提到的精确打击的对象。
        ·pid = 0 :表示等待与调用进程同一个进程组的任意子进程;因为子进程可以设置自己的进程组,所以某些子进程不一定和父进程归属于同一个进程组,这样的子进程, waitpid 函数就毫不关心了。
        ·pid = -1 :表示等待任意子进程,同 wait 类似。 waitpid( -1,&status,0 )与 wait(&status)完全等价。
        ·pid < -1 :等待所有子进程中,进程组 ID 与 pid 绝对值相等的所有子进程。    
       status: 与wait()相同,都是用来记录子进程的相关事件
       options:
          WUNTRACE :除了关心终止子进程的信息,也关心那些因信号而停止的子进程信息。
          WCONTINUED :除了关心终止子进程的信息,也关心那些因收到信号而恢复执行的子进程的状态信息。
          WNOHANG :指定的子进程并未发生状态变化,立刻返回,不会阻塞。这种情况下返回值是 0 。如果调用进程并没有与 pid 匹配的子进程,则返回 -1 ,并设置 errno 为 ECHILD 
       返回值: 与wait()相同,都是终止子进程或因信号停止或因信号恢复而执行的子进程的进程 ID 。

    (1)waitpid的致命缺陷
        当 waitpid 返回时,可能是因为子进程终止,也可能是因为子进程停止。这是 waitpid 和 wait 的致命缺陷。
        ==>为了解决这个缺陷, wait 家族的最重要成员, waitid ()函数就要闪亮登场了。
        
④等待子进程之等待状态值
    无论是 wait ()函数还是 waitpid ()函数,都有一个 status 变量。这个变量是一个 int 型指针。可以传递 NULL ,表示不关心子进程的状态信息。如果不为空,则根据填充的 status 值,可以获取到子进程的很多信息。

    (1)进程是正常退出的
        WIFEXITED(status)        // 如果子进程正常退出, 返回ture,反之false
        WEXITSTATUS(status)        // 如果子进程正常退出, 该宏获取进程的退出状态

    (2)进程收到信号,导致退出    
        WIFSIGNALED(status)        // 如果子进程被信号杀死, 返回ture,反之false
        WTERMSIG(status)        // 如果子进程被信号杀死, 该宏获取进程的退出状态
        WCOREDUMP(status)        // 如果子进程产生了core dump,  返回ture,反之false

    (3)进程收到信号,被停止
        WIFSTOPPED(status)        // 如果子进程被信号暂停, 返回ture,反之false
        WSTOPSIG(status)        // 如果子进程处于暂停状态, 该宏返回信号值

    (4)子进程恢复执行
        WIFCONTINUED(status)    // 如果子进程收到SIG_CONT信号而恢复运行, 返回ture,反之false
    使用示例:
        printf("status = %d\n",status);
        if(WIFEXITED(status))
            printf("normal termination,exit status = %d\n",WEXITSTATUS(status));

    (5)如果用户不关心子进程的终止事件,只关心子进程的停止事件,能否使用 waitpid ()明确做到?答案是不行。当 waitpid 返回时,可能是因为子进程终止,也可能是因为子进程停止。这是 waitpid 和 wait 的致命缺陷。


⑤等待子进程之waittid()
    说明:
        waitpid 函数是 wait 函数的超集, wait 函数能干的事情, waitpid 函数都能做到。但是 waitpid 函数的控制还是不太精确,无论用户是否关心相关子进程的终止事件,终止事件都可能会返回给用户。 glibc 封装了 waitid 系统调用从而实现了waitid 函数。尽管目前普遍使用的是 wait 和 waitpid 两个函数,但是 waitid 函数的设计显然更加合理。

    int waitid(idtype_t idtype, id_t id,siginfo_t *infop, int options);
      idtype:    
        ·idtype==P_PID :精确打击,等待进程 ID 等于 id 的进程。
        ·idtype==P_PGID :在所有子进程中等待进程组 ID 等于 id 的进程。
        ·idtype==P_ALL :等待任意子进程,第二个参数 id 被忽略。    
      options:
        ·WEXITED :等待子进程的终止事件。
        ·WSTOPPED :等待被信号暂停的子进程事件。
        ·WCONTINUED :等待先前被暂停,但是被 SIGCONT 信号恢复执行的子进程。    
     infop: 本质是个返回值,系统调用负责将子进程的相关信息填充到 infop 指向的结构体中。如果成功获取到信息,下面的字段将会被填充:
        ·si_pid :子进程的进程 ID ,相当于 wait 和 waitpid 成功时的返回值。
        ·si_uid :子进程真正的用户 ID 。
        ·si_signo :该字段总被填成 SIGCHLD 。
        ·si_code :指示子进程发生的事件,该字段可能的取值是:    
▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁                    
【4.8】exec家族
    1.整个 exec 家族有 6 个函数,这些函数都是构建在 execve 系统调用之上的。该系统调用的作用是,将新程序加载到进程的地址空间,丢弃旧有的程序,进程的栈、数据段、堆栈等会被新程序替换。  

    2.接口虽然各异,实现的功能却是相同的。
        // 带 p 的表示可以使用环境变量 PATH ,带 e 的表示必须要自己维护环境变量,而不使用当前环境变量
        // 采用列表,它们会罗列所有的参数(l,表示list)
        int execl(const char *path, const char *arg, ...)
        int execlp(const char *file, const char *arg, ...)
        int execle(const char *path, const char *arg,..., char * const envp[])
        
        // 采用数组罗列所有的参数(v,表示vector)
        int execv(const char *path, char *const argv[])
        int execvp(const char *file, char *const argv[])
        int execve(const char *path, char *const argv[],char *const envp[])

    3.使用范例
        char *const ps_argv[] = {"ps","-ax",NULL};
        char *const ps_envp[] = {"PATH=/bin:/usr/bin","TERM=console",NULL};
        
        execl("/bin/ps","ps","-ax",NULL);
        execlp("ps","ps","-ax",NULL);               /* 带p 的,可以使用环境变量PATH ,无须写全路径*/
        execle("/bin/ps","ps","-ax",NULL,ps_envp); /* 带e 的需要自己组拼环境变量*/
        execv("/bin/ps",ps_argv);            
        execvp("ps",ps_argv);                /* 带p 的,可以使用环境变量PATH ,无须写全路径*/
        execve("/bin/ps",ps_argv,ps_envp);    /* 带e 的需要自己组拼环境变量*/    

    4.执行 exec 之后进程继承的属性    
        1.执行 exec 的进程,其个性虽然叛逆,与过去做了决裂,但是也继承了过去的一些属性。 
        2.exec 运行之后,与进程相关的 ID 都保持不变。如果进程在执行 exec 之前,设置了告警(如调用了 alarm 函数),那么在告警时间到时,它仍然会产生一个信号。在执行 exec 后,挂起信号依然保留。
        3.创建文件时,掩码 umask 和执行 exec 之前一样。

▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁                    
【4.9】system 函数
①接口
    int system(const char *command);
        command: 需要执行的命令 
        返回值:
            1.当 command 为 NULL 时,返回 0 或 1
            2.创建进程( fork )失败,或者获取子进程终止状态( waitpid )失败,则返回 -1
            3.如果子进程不能执行 shell ,那么 system 返回值会与 _exit ( 127 )终止时一样
            4.如果所有的系统调用都执行成功, system 函数就会返回执行 command 的子 shell 的终止状态
    1.程序可以通过调用 system 函数,来执行任意的 shell 命令。
    2.system的效率低
        使用 system 运行命令时,一般要创建两个进程,一个是 shell 进程,另外一个或多个是用于 shell 所执行的命令。

②system 函数与信号
    1.就是调用 system 函数,在 system 返回之前会忽略 SIGINT 和 SIGQUIT ,无论是调用采用终端的操作( ctrl+c 或 ctrl+\ ),还是采用 kill 来发送 SIGINT 或 SIGQUIT 信号,调用 system 函数的进程都会不动如山。
    2.但是 system 内部创建的执行 command 的子进程,对 SIGINT 和 SIGQUIT 的响应是默认值,也就是说会杀掉响应的子进程而导致 system 函数的返回。
    
 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值