• 【linux】进程的概念与控制


    目录

    冯诺依曼体系结构

    操作系统(Operator System)

    进程 

    基本概念

    组织进程

    查看进程

    进程状态

     僵尸进程危害

    环境变量

    程序地址空间

    挂起

    进程创建

    写时拷贝

    进程终止

    _exit函数

    exit函数

    参数:


    冯诺依曼体系结构

    我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系
    截至目前,我们所认识的计算机,都是有一个个的硬件组件组成

    关于冯诺依曼,必须强调几点:

    这里的存储器指的是内存不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。一句话,所有设备都只能直接和内存打交道。

    操作系统(Operator System)

    概念
    任何计算机系统都包含一个基本的程序集合,称为操作系统 (OS) 。笼统的理解,操作系统包括:
    内核(进程管理,内存管理,文件管理,驱动管理)其他程序(例如函数库, shell 程序等等)
    设计 OS 的目的
    与硬件交互,管理所有的软硬件资源
    为用户程序(应用程序)提供一个良好的执行环境
    定位
    在整个计算机软硬件架构中,操作系统的定位是: 一款纯正的 搞管理 的软件
    如何理解 " 管理 "
    总结
    计算机管理硬件
    1. 描述起来,用struct结构体
    2. 组织起来,用链表或其他高效的数据结构
    系统调用和库函数概念
    在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
    系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。

    进程 

    基本概念

    课本概念:程序的一个执行实例,正在执行的程序等
    内核观点:担当分配系统资源( CPU 时间,内存)的实体。
    描述进程-PCB
    进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
    课本上称之为 PCB process control block ), Linux 操作系统下的 PCB : task_struct
    task_struct-PCB 的一种
    Linux 中描述进程的结构体叫做 task_struct。task_struct Linux 内核的一种数据结构,它会被装载到 RAM( 内存 ) 里并且包含着进程的信息。
    task_ struct 内容分类
    标示符: 描述本进程的唯一标示符,用来区别其他进程。
    状态: 任务状态,退出代码,退出信号等。
    优先级: 相对于其他进程的优先级。
    程序计数器: 程序中即将被执行的下一条指令的地址。
    内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
    上下文数据 : 进程执行时处理器的寄存器中的数据 [ 休学例子,要加图 CPU ,寄存器 ]
    I O 状态信息 : 包括显示的 I/O 请求 , 分配给进程的 I O 设备和被进程使用的文件列表。

    组织进程

    可以在内核源代码里找到它。所有运行在系统里的进程都以 task_struct 链表的形式存在内核里。

    查看进程

    进程的信息可以通过 /proc 系统文件夹查看
    如:要获取 PID 1 的进程信息,你需要查看 /proc/1 这个文件夹。

    大多数进程信息同样可以使用topps这些用户级工具来获取 

    1. #include
    2. #include
    3. #include
    4. int main()
    5. {
    6. while(1){
    7. sleep(1);
    8. }
    9. return 0;
    10. }

    通过系统调用获取进程标示符
    进程 id PID
    父进程 id PPID
    1. #include
    2. #include
    3. #include
    4. int main()
    5. {
    6. printf("pid: %d\n", getpid());
    7. printf("ppid: %d\n", getppid());
    8. return 0;
    9. }
    fork 之后通常要用 if 进行分流
    1. 父进程调用fork,返回子线程pid(>0)

    2. 子进程调用fork,子进程返回0,调用失败的话就返回-1

    1. #include
    2. #include
    3. #include
    4. int main()
    5. {
    6. int ret = fork();
    7. if(ret < 0){
    8. perror("fork");
    9. return 1;
    10. }
    11. else if(ret == 0){ //child
    12. printf("I am child : %d!, ret: %d\n", getpid(), ret);
    13. }else{ //father
    14. printf("I am father : %d!, ret: %d\n", getpid(), ret);
    15. }
    16. sleep(1);
    17. return 0;
    18. }

    进程状态

    看看 Linux 内核源代码怎么说
    为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux 内核里,进程有时候也叫做任务)。
    1. /*
    2. * The task state array is a strange "bitmap" of
    3. * reasons to sleep. Thus "running" is zero, and
    4. * you can test for combinations of others with
    5. * simple bit tests.
    6. */
    7. static const char * const task_state_array[] = {
    8. "R (running)", /* 0 */
    9. "S (sleeping)", /* 1 */
    10. "D (disk sleep)", /* 2 */
    11. "T (stopped)", /* 4 */
    12. "t (tracing stop)", /* 8 */
    13. "X (dead)", /* 16 */
    14. "Z (zombie)", /* 32 */
    15. };
    R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
    S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
    (interruptible sleep))。
    D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
    T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
    X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态

    进程状态查看
    ps aux / ps axj 命令

    Z(zombie)- 僵尸进程
    僵死状态( Zombies )是一个比较特殊的状态。当进程退出并且父进程(使用 wait() 系统调用
    没有读取到子进程退出的返回代码时就会产生僵死 ( ) 进程
    僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
    所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入 Z 状态

    1. #include
    2. #include
    3. int main()
    4. {
    5. pid_t id = fork();
    6. if(id < 0){
    7. perror("fork");
    8. return 1; }
    9. else if(id > 0){ //parent
    10. printf("parent[%d] is sleeping...\n", getpid());
    11. sleep(30);
    12. }else{
    13. printf("child[%d] is begin Z...\n", getpid());
    14. sleep(5);
    15. exit(EXIT_SUCCESS);
    16. }
    17. return 0;
    18. }

     僵尸进程危害

    进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
    维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
    那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!内存泄漏?是的!
    孤儿进程
    父进程如果提前退出,那么子进程后退出,进入 Z 之后,那该如何处理呢?
    父进程先退出,子进程就称之为 孤儿进程
    孤儿进程被 1 init 进程领养,当然要有 init 进程回收喽。
    1. #include
    2. #include
    3. #include
    4. int main()
    5. {
    6. pid_t id = fork();
    7. if(id < 0){
    8. perror("fork");
    9. return 1;
    10. }
    11. else if(id == 0){//child
    12. printf("I am child, pid : %d\n", getpid());
    13. sleep(10);
    14. }else{//parent
    15. printf("I am parent, pid: %d\n", getpid());
    16. sleep(3);
    17. exit(0);
    18. }
    19. return 0;
    20. }

    进程优先级
    基本概念
    cpu 资源分配的先后顺序,就是指进程的优先权( priority )。
    优先权高的进程有优先执行权利。配置进程优先权对多任务环境的 linux很有用,可以改善系统性能。 还可以把进程运行到指定的 CPU 上,这样一来,把不重要的进程安排到某个 CPU,可以大大改善系统整体性能。
    查看系统进程
    linux 或者 unix 系统中,用 ps –l 命令则会类似输出以下几个内容
    我们很容易注意到其中的几个重要信息,有下:
    UID : 代表执行者的身份
    PID : 代表这个进程的代号
    PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
    PRI :代表这个进程可被执行的优先级,其值越小越早被执行
    NI :代表这个进程的nice值
    PRI and NI
    PRI 也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高那 NI ? 就是我们所要说的 nice值了,其表示进程可被执行的优先级的修正数值PRI 值越小越快被执行,那么加入 nice 值后,将会使得 PRI 变为: PRI(new)=PRI(old)+nice
    这样,当 nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行所以,调整进程优先级,在 Linux 下,就是调整进程 nice值nice 其取值范围是 -20 19 ,一共 40 个级别。
    PRI vs NI
    需要强调一点的是,进程的 nice 值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。可以理解 nice 值是进程优先级的修正修正数据

    环境变量

    基本概念
    环境变量 (environment variables) 一般是指在操作系统中用来指定操作系统运行环境的一些参数
    如:我们在编写 C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
    环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
    常见环境变量
    PATH : 指定命令的搜索路径
    HOME : 指定用户的主工作目录 ( 即用户登陆到 Linux 系统中时 , 默认的目录 )
    SHELL : 当前 Shell, 它的值通常是 /bin/bash
    查看环境变量方法
    echo $NAME //NAME: 你的环境变量名称

    程序地址空间

    研究背景
    kernel 2.6.32
    32 位平台
    程序地址空间回顾

     

    1. #include
    2. #include
    3. #include
    4. int g_val = 0;
    5. int main()
    6. {
    7. pid_t id = fork();
    8. if(id < 0){
    9. perror("fork");
    10. return 0;
    11. }
    12. else if(id == 0){ //child
    13. printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
    14. }else{ //parent
    15. printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
    16. }
    17. sleep(1);
    18. return 0;
    19. }

     输出

    // 与环境相关,观察现象即可
    parent[2995]: 0 : 0x80497d8
    child[2996]: 0 : 0x80497d8
    我们发现,输出出来的变量值和地址是一模一样的,因为子进程按照父进程为模版,父子并没有对变量进行进行任何修改。可是将代码稍加改动 :
    1. #include
    2. #include
    3. #include
    4. int g_val = 0;
    5. int main()
    6. {
    7. pid_t id = fork();
    8. if(id < 0){
    9. perror("fork");
    10. return 0;
    11. }
    12. else if(id == 0){ //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
    13. g_val=100;
    14. printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
    15. }else{ //parent
    16. sleep(3);
    17. printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
    18. }
    19. sleep(1);
    20. return 0;
    21. }
    输出结果
    // 与环境相关,观察现象即可
    child[3046]: 100 : 0x80497e8
    parent[3045]: 0 : 0x80497e8
    我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
    变量内容不一样 , 所以父子进程输出的变量绝对不是同一个变量

    但地址值是一样的,说明,该地址绝对不是物理地址!
    Linux 地址下,这种地址叫做 虚拟地址
    我们在用 C/C++ 语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由 OS 统一管理
    OS 必须负责将 虚拟地址 转化成 物理地址
    进程地址空间
    所以之前说 程序的地址空间 是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看图:

    进程地址空间

    • 使用物理地址不安全,所以使用虚拟地址
    • 每一个进程有自己的pcb
    • 操作系统给每一个进程创造虚拟地址空间

    深入理解虚拟地址

    • 虚拟地址会通过映射机制来访问实际的物理内存物理地址(页表)
    • 地址空间不要仅仅理解为是os内部要遵守的,其实编译器也要遵守,即编译器编译代码的时候,就已经给我们形成了,各个区域,并且采用和linux内核一样的编址方式,给每一个变量,每一行代码都进行了编址,所以程序在编译的时候,每一个字段早已经具有了一个虚拟地址
    • Cpu拿到的都是虚拟地址,通过页表跳转读取到物理地址的内容
    • 本质上,因为有地址空间的存在,所以上层申请空间,其实是在地址空间上申请的,而并不是物理内存
    • 只有当你真正的通过虚拟地址传递到cpu再访问物理地址空间的时候(是由操作系统自动完成,用户包括进程完全没有感知),才会执行内存相关的管理算法,帮你申请内存,构建页表映射关系,然后在进行内存的访问

    因为在物理内存中理论上可以任意位置加载,那么是不是物理内存的所有数据和代码都是乱序的?

    没错,但是因为有页表的存在,它可以将地址空间上的虚拟地址和物理地址进行映射,

    那么是不是在进程视角所有内存分布都是有序的?

    地址空间+页表的存在,可以将内存分布有序化。

    进程要访问的物理内存中的数据和代码,可能并没有在物理内存中,同样也可以让不同的进程映射到不同的物理内存,很容易让进程独立性的实现

    进程的独立性可以通过地址空间+页表的方式实现。

    因为有地址空间的存在,每一个进程都认为自己拥有4gb的空间,并且各个区域是有序的,进而可以通过页表映射到不同的区域,来实现进程的独立性。

    所以每一个进程是不需要知道其他进程的存在的。·

    加载本质是创建进程,那么是不是必须非要把所有程序的代码和数据加载到内存中,并且创建内核数据结构建立映射关系?

    在最极端的情况下,甚至只有内核结构被创建出来了。

    理论上可以实现对程序的分批加载

    挂起

    进程的数据和代码被换出了,就叫被挂起

    创建子进程,不需要将不会被访问的或者只会读取的数据拷贝一份

    只有将来会被父或者子进程写入的数据,值得被拷贝。一般来说即便是os,也无法提前知道哪些空间可能会被写入,所以os选择写时拷贝技术,来进行将父子进程的数据分离。

    进程终止时,操作系统做了什么

    要释放进程申请的相关内核数据结构和对应的数据和代码。本质就是释放系统资源


    进程创建

    fork 函数初识
    linux fork 函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
    1. #include
    2. pid_t fork(void);
    3. 返回值:自进程中返回0,父进程返回子进程id,出错返回-1
    进程调用 fork ,当控制转移到内核中的 fork 代码后,内核做:
    分配新的内存块和内核数据结构给子进程
    将父进程部分数据结构内容拷贝至子进程
    添加子进程到系统进程列表当中
    fork 返回,开始调度器调度

     

    当一个进程调用 fork 之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以 开始它们自己的旅程,看如下程序
    1. int main( void )
    2. {
    3. pid_t pid;
    4. printf("Before: pid is %d\n", getpid());
    5. if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
    6. printf("After:pid is %d, fork return %d\n", getpid(), pid);
    7. sleep(1);
    8. return 0;
    9. }
    10. 运行结果:
    11. [root@localhost linux]# ./a.out
    12. Before: pid is 43676
    13. After:pid is 43676, fork return 43677
    14. After:pid is 43677, fork return 0
    这里看到了三行输出,一行 before ,两行 after 。进程 43676 先打印 before 消息,然后它又打印 after 。另一个 after
    消息有 43677 打印的。注意到进程 43677 没有打印before,为什么呢?如下图所示

    所以, fork 之前父进程独立执行, fork 之后,父子两个执行流分别执行。注意, fork 之后,谁先执行完全由调度器 决定。
    fork 函数返回值
    子进程返回0,
    父进程返回的是子进程的pid 

    写时拷贝

    通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图
    fork 常规用法
    一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
    一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
    fork 调用失败的原因
    系统中有太多的进程
    实际用户的进程数超过了限制

    进程终止

    进程退出场景
    代码运行完毕,结果正确
    代码运行完毕,结果不正确
    代码异常终止
    进程常见退出方法
    正常终止(可以通过 echo $? 查看进程退出码):
    1. main 返回
    2. 调用 exit
    3. _exit
    异常退出:
    ctrl + c ,信号终止

    _exit函数

    1. #include
    2. void _exit(int status);
    3. 参数:status 定义了进程的终止状态,父进程通过wait来获取该值
    说明:虽然 status int ,但是仅有低 8 位可以被父进程所用。所以 _exit(-1) 时,在终端执行 $?发现返回值是 255

    exit函数

    1. #include
    2. void exit(int status);

    exit最后也会调用exit, 但在调用exit之前,还做了其他工作:

    1. 执行用户通过 atexit on_exit 定义的清理函数。
    2. 关闭所有打开的流,所有的缓存数据均被写入
    3. 调用_exit  
    1. int main()
    2. {
    3. printf("hello");
    4. exit(0);
    5. }
    6. 运行结果:
    7. [root@localhost linux]# ./a.out
    8. hello[root@localhost linux]#
    9. int main()
    10. {
    11. printf("hello");
    12. _exit(0);
    13. }
    14. 运行结果:
    15. [root@localhost linux]# ./a.out
    16. [root@localhost linux]#

     return退出

    return 是一种更常见的退出进程方法。执行 return n 等同于执行 exit(n), 因为调用 main 的运行时函数会将 main的返回值当做 exit 的参数。

    进程等待

    进程等待必要性
    • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏。
    • 另外,进程一旦变成僵尸状态,那就刀枪不入,杀人不眨眼kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
    • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
    • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
    进程等待的方法
    wait方法
    #include
    #include
    pid_t wait(int*status);
    返回值:
    • 成功返回被等待进程pid,失败返回-1
    参数:
    输出型参数,获取子进程退出状态 , 不关心则可以设置成为NULL        
    waitpid 方法
    pid_ t waitpid(pid_t pid, int *status, int options);
    返回值:
    当正常返回的时候waitpid返回收集到的子进程的进程ID;
    如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
    如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

    参数:

    pid
    Pid=-1,等待任一个子进程。与wait等效。
    Pid>0.等待其进程ID与pid相等的子进程。
    status:
    WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
    WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
    options:
    WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
    如果子进程已经退出,调用 wait/waitpid 时, wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
    如果在任意时刻调用 wait/waitpid ,子进程存在且正常运行,则进程可能阻塞。
    如果不存在该子进程,则立即出错返回。

    获取子进程 status
    wait waitpid ,都有一个 status参数,该参数是一 个输出型参数,由操作系统填充。
    如果传递 NULL ,表示不关心子进程的退出状态信息。
    否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
    status 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究 status低16比特位):
    1. 测试代码:
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main( void )
    8. {
    9. pid_t pid;
    10. if ( (pid=fork()) == -1 )
    11. perror("fork"),exit(1);
    12. if ( pid == 0 ){
    13. sleep(20);
    14. exit(10);
    15. } else {
    16. int st;
    17. int ret = wait(&st);
    18. if ( ret > 0 && ( st & 0X7F ) == 0 ){ // 正常退出
    19. printf("child exit code:%d\n", (st>>8)&0XFF);
    20. } else if( ret > 0 ) { // 异常退出
    21. printf("sig code : %d\n", st&0X7F );
    22. }
    23. }
    24. }
    25. 测试结果:
    26. [root@localhost linux]# ./a.out #等20秒退出
    27. child exit code:10
    28. [root@localhost linux]# ./a.out #在其他终端kill掉
    29. sig code : 9

     具体代码实现

    进程的阻塞等待方式
    1. int main()
    2. {
    3. pid_t pid;
    4. pid = fork();
    5. if(pid < 0){
    6. printf("%s fork error\n",__FUNCTION__);
    7. return 1;
    8. } else if( pid == 0 ){ //child
    9. printf("child is run, pid is : %d\n",getpid());
    10. sleep(5);
    11. exit(257);
    12. } else{
    13. int status = 0;
    14. pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
    15. printf("this is test for wait\n");
    16. if( WIFEXITED(status) && ret == pid ){
    17. printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
    18. }else{
    19. printf("wait child failed, return.\n");
    20. return 1;
    21. }
    22. }
    23. return 0;
    24. }
    25. 运行结果:
    26. [root@localhost linux]# ./a.out
    27. child is run, pid is : 45110
    28. this is test for wait
    29. wait child 5s success, child return code is :1.
    进程的非阻塞等待方式:
    1. #include
    2. #include
    3. #include
    4. #include
    5. int main()
    6. {
    7. pid_t pid;
    8. pid = fork();
    9. if(pid < 0){
    10. printf("%s fork error\n",__FUNCTION__);
    11. return 1;
    12. }else if( pid == 0 ){ //child
    13. printf("child is run, pid is : %d\n",getpid());
    14. sleep(5);
    15. exit(1);
    16. } else{
    17. int status = 0;
    18. pid_t ret = 0;
    19. do
    20. {
    21. ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
    22. if( ret == 0 ){
    23. printf("child is running\n");
    24. }
    25. sleep(1);
    26. }while(ret == 0);
    27. if( WIFEXITED(status) && ret == pid ){
    28. printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
    29. }else{
    30. printf("wait child failed, return.\n");
    31. return 1;
    32. }
    33. }
    34. return 0;
    35. }
    进程程序替换
    替换原理
    fork 创建子进程后执行的是和父进程相同的程序 ( 但有可能执行不同的代码分支 ), 子进程往往要调用一种 exec 函数
    以执行另一个程序。当进程调用一种 exec 函数时 , 该进程的用户空间代码和数据完全被新程序替换 , 从新程序的启动
    例程开始执行。调用 exec 并不创建新进程 , 所以调用 exec 前后该进程的id并未改变

  • 相关阅读:
    vue2 打印数据 以及使用 (2)
    Linux Ubuntu系统中添加磁盘
    水球图-利用动态波纹展示百分比
    观察者(observer)模式(一)
    时光机特效什么app好?建议收藏这些软件
    展开语法、剩余语法
    网站SEO优化
    【动态规划】- 概述 + js 求解爬楼梯问题
    老卫带你学---leetcode刷题(4. 寻找两个正序数组的中位数)
    参数查阅~~~9.18
  • 原文地址:https://blog.csdn.net/yahouhou_/article/details/128197551