uc笔记07---进程管理,PID,#ps,getxxxid,fork,vfork,system

本文深入解析了进程的基础概念,包括进程与程序的区别、进程的分类、查看进程的方法等,并详细介绍了进程标识符、进程间的父子关系及孤儿进程和僵尸进程的概念。此外,还探讨了进程创建(fork/vfork)、进程退出、等待子进程终止、执行新进程(exec)及系统调用(system)等核心操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.    进程与程序
    1) 进程就是运行中的程序。一个运行着的程序,
    可能有多个进程。进程在操作系统中执行特定的任务。

    2) 程序是存储在磁盘上,包含可执行机器指令和数据的静态实体。
    进程或者任务是处于活动状态的计算机程序。

2.    进程的分类
    1) 进程一般分为 交互进程、批处理进程 和 守护进程三类。

    2) 守护进程总是活跃的,一般是后台运行。
    守护进程一般是由系统在开机时通过脚本自动激活启动,或者由超级用户 root 来启动。

3.    查看进程
    1) 简单形式
    # ps
    以简略方式显示当前用户有控制终端的进程信息。

    2) BSD 风格常用选项
    # ps axu

    a - 所有用户有控制终端的进程
    x - 包括无控制终端的进程
    u - 以详尽方式显示
    w - 以更大列宽显示

    3) SVR4 风格常用选项
    # ps -efl
    # ps -efl | grep 10086    // 查看具体某一个进程
    # ps -efl | grep a.out
    这里 | 是管道符,意思是把前者的输出当作后者的输入;

    -e或-A            - 所有用户的进程
    -a                - 当前终端的进程
    -u 用户名或用户ID   - 特定用户的进程    // #ps -u root
    -g 组名或组ID      - 特定组的进程        // #ps -g tarena
    -f                - 按完整格式显示
    -F                - 按更完整格式显示
    -l                - 按长格式显示

    4) 进程信息列表
    USER/UID: 进程属主。
    PID: 进程 ID。
    %CPU/C: CPU 使用率。
    %MEM: 内存使用率。
    VSZ: 占用虚拟内存大小(KB)。
    RSS: 占用物理内存大小(KB)。
    TTY: 终端次设备号,“?”表示无控制终端,如后台进程。
        pts/1:虚拟终端,tty3:实体终端;
    STAT/S: 进程状态。可取如下值:
        O - 就绪。等待被调度。
        R - 运行。Linux 下没有 O 状态,就绪状态也用 R 表示。
        S - 可唤醒睡眠。系统中断,获得资源,收到信号,都可被唤醒,转入运行状态。
        D - 不可唤醒睡眠。只能被 wake_up 系统调用唤醒。
        T - 暂停。收到 SIGSTOP 信号转入暂停状态,收到 SIGCONT 信号转入运行状态。
        W - 等待内存分页 (2.6 内核以后被废弃)。
        X - 死亡。不可见。
        Z - 僵尸。已停止运行,但其父进程尚未获取其状态。
        < - 高优先级。
        N - 低优先级。
        L - 有被锁到内存中的分页。实时进程和定制 IO。
        s - 会话首进程。
        l - 多线程化的进程。
        + - 在前台进程组中。
    START/STIME: 进程开始时间。
    TIME: 进程运行时间。
    COMMAND/CMD: 进程指令。
    F: 进程标志。可由下列值取和:
        1 - 通过 fork 产生但是没有 exec。
        4 - 拥有超级用户特权。
    PPID: 父进程 ID。
        #ps -f
    NI: 进程 nice 值,-20 到 19,可通过系统调用或命令修改。
    PRI: 进程优先级。
        #ps -lF
        静态优先级 = 80 + nice,60 到 99,值越小优先级越高。
        内核在静态优先级的基础上,根据进程的交互性计算得到实际(动态)优先级,
        以体现对 IO 消耗型进程的奖励,和对处理器消耗型进程的惩罚。
    ADDR: 内核进程的内存地址。普通进程显示“-”。
    SZ: 占用虚拟内存页数。
    WCHAN: 进程正在等待的内核函数或事件。
    PSR: 进程被绑定到哪个处理器。
    
    例如:
    # ps axu
    
    USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    root         1  0.0  0.0   2172   668 ?        Ss   09:03   0:00 init [5]      
    root         2  0.0  0.0      0     0 ?        S<   09:03   0:00 [migration/0]
    root         3  0.0  0.0      0     0 ?        SN   09:03   0:00 [ksoftirqd/0]
    root         4  0.0  0.0      0     0 tty4     S<   09:03   0:00 [watchdog/0]
    root         5  0.0  0.0      0     0 pts/1    S<   09:03   0:00 [events/0]
    . . .
    
    # ps -l
    F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
    0 S   500  3223  3217  0  75   0 -  1512 wait   pts/1    00:00:00 bash
    0 R   500  4561  3223  0  77   0 -  1356 -      pts/1    00:00:00 ps

4.    父进程、子进程、孤儿进程和僵尸进程
        内核进程(0)
          init(1)
            xinetd
              in.telnetd <- 用户登录
                login
                  bash
                    vi

    1) 父进程启动子进程后,
    子进程在操作系统的调度下与其父进程同时运行。

    2) 子进程先于父进程结束,
    子进程向父进程发送 SIGCHLD(17) 信号,父进程回收子进程的相关资源。
    如果父进程没有回收子进程的相关资源,该子进程即成为僵尸进程。

    3) 父进程先于子进程结束,子进程成为孤儿进程,
    同时被 init 进程收养,即成为 init 进程的子进程。
    
    tips:孤儿进程和僵尸进程不可能同时存在一个进程上;

5.    进程标识符(进程 ID)
    1) 每个进程都有一个以非负整数表示的唯一标识,即进程 ID/PID。
    2) 进程 ID 在任何时刻都是唯一的,但可以重用,当一个进程退出时,其进程 ID 就可以被其它进程使用。
    3) 延迟重用。

    范例:delay.c
        #include <stdio.h>
        int main (void) {
            printf ("进程ID:%u\n", getpid ());
            return 0;
        }
        运行测试:
        #a.out
        进程ID:16431
        #a.out
        进程ID:16432
        #a.out
        进程ID:16433
        #a.out
        进程ID:16431

6.    getxxxid

    #include <unistd.h>
    getpid    - 获取进程ID
    getppid    - 获取父进程ID
    getuid    - 获取实际用户ID(登录用户 ID)
    geteuid    - 获取有效用户ID(属主 ID)
    getgid    - 获取实际组ID
    getegid    - 获取有效组ID

    范例:id.c
        #include <stdio.h>
        int main (void) {
            printf ("    进程ID:%u\n", getpid ());
            printf ("  父进程ID:%u\n", getppid ());
            printf ("实际用户ID:%u\n", getuid ());
            printf ("有效用户ID:%u\n", geteuid ());
            printf ("  实际组ID:%u\n", getgid ());
            printf ("  有效组ID:%u\n", getegid ());
            return 0;
        }
    以其它用户身份登录并执行
    # a.out
    输出
        进程ID:16443
      父进程ID:15890
    实际用户ID:1000 - 实际用户 ID 取父进程(shell)的实际用户 ID
    有效用户ID:1000 - 有效用户 ID 取实际用户 ID
      实际组ID:1000 - 实际组 ID 取父进程(shell)的实际组 ID
      有效组ID:1000 - 有效组 ID 取实际组 ID
    
    查看进程 ID:
    #ps -eaf | grep 15890

    执行
    # ls -l a.out
    输出
    -rwxr-xr-x ...
       ^   ^
    为a.out的文件权限添加设置用户ID位和设置组ID位
    # chmod u+s a.out
    # chmod g+s a.out

    执行
    # ls -l a.out
    输出
    -rwsr-sr-x ...
       ^   ^
    以其它用户身份登录并执行
    # a.out
    输出
        进程ID:...
      父进程ID:...
    实际用户ID:1000 - 实际用户 ID 取父进程(shell)的实际用户 ID
    有效用户ID:0    - 有效用户 ID 取程序文件的属主 ID
      实际组ID:1000 - 实际组 ID 取父进程(shell)的实际组 ID
      有效组ID:0    - 有效组 ID 取程序文件的属组 ID

    进程的访问权限由其有效用户 ID 和有效组 ID 决定。
    通过此方法可以使进程获得比登录用户更高的权限。
    比如通过 passwd 命令修改登录口令:
    
    执行
    ls -l /etc/passwd
    输出
    -rw-r--r--. 1 root root 1648 Nov  9 14:05 /etc/passwd
      ^
    该文件中存放所有用户的口令信息,仅 root 用户可写,
    但事实上任何用户都可以修改自己的登录口令,
    即任何用户都可以通过 /usr/bin/passwd 程序写该文件:
    
    执行
    # ls -l /usr/bin/passwd
    输出
    -rwsr-xr-x. 1 root root 28816 Feb  8  2011 /usr/bin/passwd
       ^          ^
    该程序具有设置用户 ID 位,且其属主为 root。
    因此以任何用户登录系统,执行 passwd 命令所启动的进程,
    其有效用户 ID 均为 root,对 /etc/passwd 文件有写权限。

7.    fork

    #include <unistd.h>
    pid_t fork (void);

    1)创建一个子进程,失败返回 -1。
    2)调用一次,返回两次;在父进程中返回子进程的 PID;在子进程中返回 0;
    利用返回值的不同,可以分别为父/子进程编写不同的处理分支。

    范例:fork.c
        #include <stdio.h>
        #include <unistd.h>
        int main (void) {
            printf ("%u进程:我要调用fork()了...\n", getpid ());
            pid_t pid = fork ();
            if (pid == -1) {
                perror ("fork");
                return -1;
            }
            if (pid == 0) {        // fork() 在子进程里返回 0;
                printf ("%u进程:我是%u进程的子进程。\n", getpid (),
                    getppid ());
                return 0;
            }
            printf ("%u进程:我是%u进程的父进程。\n", getpid (), pid);
            sleep (1);
            return 0;
        }
        
    3)子进程是父进程的副本,
    子进程获得父进程数据段和堆栈段(包括 I/O 流缓冲区)的拷贝,
    但子进程共享父进程的代码段。

    范例:mem.c
        #include <stdio.h>
        #include <stdlib.h>
        #include <unistd.h>
        int global = 100;
        int main (void) {
            int local = 200;
            char* heap = (char*)malloc (256 * sizeof (char));
            sprintf (heap, "ABC");
            printf ("父进程:%d %d %s\n", global, local, heap);
            pid_t pid = fork ();
            if (pid == -1) {
                perror ("fork");
                return -1;
            }
            // 在子进程里对三个数据作修改
            if (pid == 0) {
                global++;
                local++;
                sprintf (heap, "XYZ");
                printf ("子进程:%d %d %s\n", global, local, heap);
                free (heap);
                return 0;
            }
            sleep (1);
            printf ("父进程:%d %d %s\n", global, local, heap);
            free (heap);
            return 0;
        }
        输出结果:
        父进程:100 200 ABC
        子进程:101 201 XYZ
        父进程:100 200 ABC
        
    范例:os.c
        #include <stdio.h>
        #include <unistd.h>
        int main (void) {
            printf ("ABC");        // 放到缓冲区
            // 开始分支
            pid_t pid = fork ();
            if (pid == -1) {
                perror ("fork");
                return -1;
            }
            if (pid == 0) {
                printf ("XYZ\n");
                return 0;
            }
            sleep (1);
            printf ("\n");
            return 0;
        }
        输出:
        ABCXYZ        // 子进程输出,把父进程的缓冲区复制过来;
        ABC        // 父进程输出;
    
    范例:is.c
        #include <stdio.h>
        #include <unistd.h>
        int main (void) {
            printf ("父进程:");
            int a, b, c;
            scanf ("%d%d%d", &a, &b, &c);
            // 开始分支
            pid_t pid = fork ();
            if (pid == -1) {
                perror ("fork");
                return -1;
            }
            if (pid == 0) {
                scanf ("%d%d%d", &a, &b, &c);
                printf ("子进程:%d %d %d\n", a, b, c);
                return 0;
            }
            sleep (1);
            printf ("父进程:%d %d %d\n", a, b, c);
            return 0;
        }
        输入:
        父进程:123456
        输出:
        子进程:456
        父进程:123

    4)函数调用后父子进程各自继续运行,
    其先后顺序不确定,某些实现可以保证子进程先被调度。

    5)函数调用后,父进程的文件描述符表(进程级)也会被复制到子进程中,
    二者共享同一个文件表(内核级)。

    范例:ftab.c
        #include <stdio.h>
        #include <string.h>
        #include <fcntl.h>
        #include <unistd.h>

        int main (void) {
            int fd = open ("ftab.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
            if (fd == -1) {
                perror ("open");
                return -1;
            }
            // 父进程写入一个字符串
            const char* text = "Hello, World !";    // 文件指针在 ! 之后那个位置
            if (write (fd, text, strlen (text) * sizeof (text[0])) == -1) {
                perror ("write");
                return -1;
            }
            // 进入子进程
            pid_t pid = fork ();
            if (pid == -1) {            // 进入子进程失败
                perror ("fork");
                return -1;
            }
            if (pid == 0) {            // 进入子进程成功
                if (lseek (fd, -7, SEEK_CUR) == -1) {    // 从当前位置向文件首移动 7 字节,应该在 W 位置
                    perror ("lseek");
                    return -1;
                }
                close (fd);    // 只能关闭子进程的文件描述符,而无法关闭文件表,应为父进程还在用;
                return 0;
            }
            sleep (1);
            // 应为文件表共享,所以在子进程里面移动了文件位置,对父进程也有影响
            text = "Linux";
            if (write (fd, text, strlen (text) * sizeof (text[0])) == -1) {
                perror ("write");
                return -1;
            }
            // 最后 ftab.txt 里面将显示:Hello Linux
            close (fd);
            return 0;
        }

    6)总进程数或实际用户 ID 所拥有的进程数,超过系统限制,该函数将失败。

    7)一个进程如果希望创建自己的副本并执行同一份代码,
    或希望与另一个程序并发地运行,都可以使用该函数。

    8)孤儿进程与僵尸进程。

    范例:orphan.c(孤儿进程)
        #include <stdio.h>
        #include <unistd.h>
        int main (void) {
            printf ("%u进程:我要调用fork()了...\n", getpid ());
            pid_t pid = fork ();
            if (pid == -1) {
                perror ("fork");
                return -1;
            }
            if (pid == 0) {
                sleep (1);    // 睡一会,等待父进程死去;
                // 验证:如果是孤儿,则 PPID 为 1(init);如果不是,则为上面的 %u 的值;
                printf ("\n%u进程:我是被%u进程收养的孤儿进程。", getpid (),
                    getppid ());
                return 0;
            }
            // 父进程打印完这条语句就死去;
            printf ("%u进程:我是%u进程的父进程。\n", getpid (), pid);
            return 0;
        }
        
    范例:zombie.c(僵尸进程)
        #include <stdio.h>
        #include <unistd.h>
        int main (void) {
            printf ("%u进程:我要调用fork()了...\n", getpid ());
            pid_t pid = fork ();
            if (pid == -1) {
                perror ("fork");
                return -1;
            }
            if (pid == 0) {
                // 打完下面这段话,子进程就死去;
                printf ("%u进程:我是%u进程的子进程,即将成为僵尸...\n",
                    getpid (), getppid ());
                return 0;
            }
            // 父进程睡一会,等子进程死去;
            sleep (1);
            printf ("%u进程:我是%u进程的父进程。\n", getpid (), pid);
            // 此时查看子进程状态
            printf ("执行ps -efl | grep %u,按<回车>退出...", pid);
            getchar ();    // 创造阻塞条件,只有当父进程结束,init 才回收子进程;
            return 0;
        }

    注意:fork 前的代码只有父进程执行,
    fork 后的代码父子进程都有机会执行,受代码逻辑的控制而进入不同分支。

8.    vfork

    #include <unistd.h>
    pid_t vfork (void);

    该函数的功能与 fork 基本相同,二者的区别:
    1)调用 vfork 创建子进程时并不复制父进程的地址空间,
    子进程可以通过 exec 函数族,直接启动另一个进程替换自身,进而提高进程创建的效率。
    2)vfork 调用之后,子进程先被调度。
    用 fork 调用的话,可能是子进程先被调用,也可能是父进程先被调用;

9.    进程的正常退出
    从 main 函数中 return(表示整个程序退出)
        int main (...) {
            . . .
            return x;
        }
    等价于:
        int main (...) {
            . . .
            exit (x);
        }

    调用标准 C语言的 exit 函数;
    在任何函数里调用 exit(); 都会直接退出整个进程;
        #include <stdlib.h>
        void exit (int status);

    1) 调用进程退出,
    其父进程调用 wait/waitpid 函数返回 status 的低 8 位。

    2) 进程退出之前,
    先调用所有事先通过 atexit/on_exit 回调函数注册的函数,
    冲刷并关闭所有仍处于打开状态的标准 I/O 流,删除所有通过 tmpfile 函数创建的文件。

    #include <stdlib.h>
    int atexit (void (*function) (void));    
    function - 函数指针,指向进程退出前需要被调用的函数。
                该函数既没有返回值也没有参数。            
    成功返回 0,失败返回非零。

    int on_exit (void (*function) (int, void*), void* arg);
    function - 函数指针,指向进程退出前需要被调用的函数。
                该函数没有返回值但有两个参数:
                第一参数来自 exit 函数的 status 参数,
                第二个参数来自 on_exit 函数的 arg 参数。
    arg      - 任意指针,将作为第二个参数被传递给 function 所指向的函数。
    成功返回 0,失败返回非零。

    3) 用 EXIT_SUCCESS/EXIT_FAILURE 常量宏
    (可能是 0/1)作参数,调用 exit() 函数表示成功/失败,提高平台兼容性。

    4) 该函数不会返回。
    因为调完该函数,进程就结束了,无法返回;
    
    5) 该函数的实现调用了 _exit/_Exit 函数。

10.    调用 _exit/_Exit 函数。

    #include <unistd.h>
    void _exit (int status);

    1) 调用进程退出,
    其父进程调用 wait/waitpid 函数返回 status 的低 8 位。

    2) 进程退出之前,
    先关闭所有仍处于打开状态的文件描述符,
    将其所有子进程托付给 init 进程(PID 为 1 的进程)收养,向父进程递送 SIGCHILD 信号。

    3) 该函数不会返回。
    4) 该函数有一个完全等价的标准 C 版本:
        #include <stdlib.h>
        void _Exit (int status);

        进程的最后一个线程执行了返回语句。
        进程的最后一个线程调用 pthread_exit 函数。
    
    5)小结:_exit 是系统调用,_Exit 是标 C 函数;
    exit 在底层调用 _exit,在调用 _exit 之前会先调用回调函数 atexit/on_exit;

    范例:exit.c
        #include <stdio.h>
        #include <stdlib.h>
        #include <unistd.h>
        void doexit1 (void) {
            printf ("doexit1()...\n");
        }
        void doexit2 (int status, void* arg) {
            printf ("doexit2(%d,%s)...\n", status, arg);
        }
        int foo (void) {
            printf ("foo()...\n");
        //    exit (EXIT_SUCCESS);
        //    exit (EXIT_FAILURE);
            exit (1234);
        //    _exit (1234);    不调用 doexit1 和 doexit2
        //    _Exit (1234);    不调用 doexit1 和 doexit2
            return 10;
        }
        int main (void) {
            if (atexit (doexit1) == -1) {
                perror ("atexit");
                return -1;
            }
            if (on_exit (doexit2, "再见") == -1) {
                perror ("on_exit");
                return -1;
            }
            printf ("foo()函数返回%d。\n", foo ());
        //    return 1234;    main 函数里面的 return 相当于调用 exit();
        }
        输出结果如下:
            foo()...
            doexit2(1234, 再见)...
            doexit1()...
        分析:主函数执行到 printf ("foo()函数返回%d。\n", foo ()); 时,
        调用 foo() 函数,foo() 调用 exit;exit 调用 atexit 和 on_exit;
        当 atexit/on_exit 执行完后,程序就结束了;
        不会执行 printf ("foo()函数返回%d。\n", foo ()); 里的 "foo()函数返回%d。\n" 语句;
        
11.    进程的异常终止
    1)调用 abort 函数,产生 SIGABRT 信号。
    2)进程接收到某些信号。
    3)最后一个线程对“取消”请求做出响应。

12.    等待子进程终止并获取其终止状态 wait/waitpid(收尸)

    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait (int* status);                                // 等待所有子进程
    pid_t waitpid (pid_t pid, int* status, int options);    // 等待具体某一个子进程
    成功返回终止子进程的 PID,失败返回 -1。

    1)当一个进程正常或异常终止时,内核向其父进程发送 SIGCHLD 信号。
    父进程可以忽略该信号,或者提供一个针对该信号的信号处理函数,默认为忽略。

    2)父进程调用 wait 函数:
    若所有子进程都在运行,则阻塞。
    若有一个子进程已终止,则返回该子进程的 PID 和终止状态 (通过 status 参数)。
    若没有需要等待子进程,则返回失败 -1,errno 为 ECHILD。

    3)在任何一个子进程终止前,wait 函数只能阻塞调用进程,而 waitpid 函数可以有更多选择。

    4)如果有一个子进程在 wait 函数被调用之前,
    已经终止并处于僵尸状态,wait 函数会立即返回,并取得该子进程的终止状态。

    5)子进程的终止状态通过输出参数 status 返回给调用者,若不关心终止状态,可将此参数置空。

    6)子进程的终止状态可借助 sys/wait.h 中定义的参数宏查看:

        WIFEXITED(): 子进程是否正常终止,是则通过 WEXITSTATUS() 宏,
        获取子进程调用 exit/_exit/_Exit 函数,所传递参数的低 8 位。
        因此传给 exit/_exit/_Exit 函数的参数最好不要超过 255。

        WIFSIGNALED(): 子进程是否异常终止,是则通过 WTERMSIG() 宏获取终止子进程的信号。

        WIFSTOPPED(): 子进程是否处于暂停,是则通过 WSTOPSIG() 宏获取暂停子进程的信号。

        WIFCONTINUED(): 子进程是否在暂停之后继续运行。

    范例:wait.c
        #include <stdio.h>
        #include <sys/wait.h>
        int main (void) {
            pid_t pid = fork ();
            if (pid == -1) {
                perror ("fork");
                return -1;
            }
            if (pid == 0) {
                int status = 0x12345678;
                printf ("子进程:我是%u进程。我要以%#x状态退出。\n", getpid (), status);
                // 输出:子进程:我是 20163 进程。我要以 0x12345678 状态退出。
                return status;
            }
            printf ("父进程:我要等待子进程...\n");
            int status;
            pid = wait (&status);        // wait 会一直等到子进程退出;
            printf ("父进程:发现%u进程以%#x状态退出了。\n", pid, WEXITSTATUS (status));
            // wait 通过 WEXITSTATUS 得到子进程终止状态;
            // WEXITSTATUS 取得所传参数 0x12345678 的低 8 位:0x78
            // 输出:父进程:发现 20163 进程以 0x78 状态退出了。
            return 0;
        }

    范例:loop.c
        #include <stdio.h>
        #include <errno.h>
        #include <unistd.h>
        int main (void) {
            int i;
            for (i = 0; i < 3; i++) {
                pid_t pid = fork ();
                if (pid == -1) {
                    perror ("fork");
                    return -1;
                }
                if (pid == 0) {
                    printf ("子进程:我是%u进程。我要退出了。\n", getpid ());
                    return 0;
                }
            }
            for (;;) {
                printf ("父进程:我要等待子进程...\n");
                pid_t pid = wait (0);        // 调一次 wait 只能回收一个子进程,所以需要不停循环;
                if (pid == -1) {                // 返回 -1 有两种情况:子进程都在运行,或者子进程都回收完了;
                    if (errno != ECHILD) {    // 异常退出;
                        perror ("wait");
                        return -1;
                    }
                    printf ("父进程:已经没有子进程可等了,走喽!\n");
                    break;
                }
                printf ("父进程:发现%u进程退出了。\n", pid);
            }
            return 0;
        }

    7)如果同时存在多个子进程,又需要等待特定的子进程,可使用 waitpid 函数,其 pid 参数:
        pid_t waitpid (pid_t pid, int* status, int options);

         -1 - 等待任一子进程,此时与 wait 函数等价。

        > 0 - 等待由该参数所标识的特定子进程。

          0 - 等待其组 ID 等于调用进程组 ID 的任一子进程,
              即等待与调用进程同进程组的任一子进程。

        <-1 - 等待其组 ID 等于该参数绝对值的任一子进程,
              即等待隶属于特定进程组内的任一子进程。

    范例:waitpid.c
        #include <stdio.h>
        #include <unistd.h>
        int main (void) {
            pid_t cpid[3];
            int i;
            for (i = 0; i < sizeof (cpid) / sizeof (cpid[0]); i++) {
                cpid[i] = fork ();
                if (cpid[i] == -1) {
                    perror ("fork");
                    return -1;
                }
                if (cpid[i] == 0) {
                    printf ("子进程:我是%u进程。我要退出了。\n", getpid ());
                    return 0;
                }
            }
            for (i = 0; i < sizeof (cpid) / sizeof (cpid[0]); i++) {
                printf ("父进程:我要等待%u进程...\n", cpid[i]);
                pid_t pid = waitpid (cpid[i], 0, 0);
                if (pid == -1) {
                    perror ("waitpid");
                    return -1;
                }
                printf ("父进程:发现%u进程退出了。\n", pid);
            }
            return 0;
        }

    8)waitpid 函数的 options 参数可取 0 (忽略)或以下值的位或:
        WNOHANG    - 非阻塞模式,若没有可用的子进程状态,则返回 0。
        WUNTRACED  - 若支持作业控制,且子进程处于暂停态,则返回其状态。
        WCONTINUED - 若支持作业控制,且子进程暂停后继续,则返回其状态。

    范例:nohang.c
        #include <stdio.h>
        #include <errno.h>
        #include <sys/wait.h>
        int main (void) {
            int i;
            for (i = 0; i < 3; i++) {
                pid_t pid = fork ();
                if (pid == -1) {
                    perror ("fork");
                    return -1;
                }
                if (pid == 0) {
                    printf ("子进程:我是%u进程。我要退出了。\n", getpid ());
                    return 0;
                }
            }
            for (;;) {
                printf ("父进程:我要等待子进程...\n");
                pid_t pid = waitpid (-1, 0, WNOHANG);
                // 以非阻塞模式等待所有子进程,且不关心终止状态
                if (pid == -1) {
                    if (errno != ECHILD) {
                        perror ("waitpid");
                        return -1;
                    }
                    printf ("父进程:已经没有子进程可等了,走喽!\n");
                    break;
                }
                if (pid)    // waitpid 返回非 0,说明子进程退出了;
                    printf ("父进程:发现%u进程退出了。\n", pid);
                else        // waitpid 返回 0,说明子进程没退出;
                    printf ("父进程:没发现子进程退出,干点儿别的...\n");
            }
            return 0;
        }

13    exec
    1)exec 函数会用新进程完全替代调用进程,并开始从 main 函数执行。
    2)exec 函数并非创建子进程,新进程取调用进程的 PID。
    3)exec 函数所创建的新进程,完全取代调用进程的代码段、数据段和堆栈段。
    4)exec 函数若执行成功,则不会返回,否则返回 -1。
    5)exec 函数包括六种形式:
    
        #include <unistd.h>
        int execl  (
            const char* path,
            const char* arg, ...
        );
        int execv  (
            const char* path,
            char* const argv[]
        );
        int execle (
            const char* path,
            const char* arg,
            . . .
            char* const envp[]
        );
        int execve (
            const char* path,
            char* const argv[],
            char* const envp[]
        );
        int execlp (
            const char* file,
            const char* arg,
            . . .
        );
        int execvp (
            const char* file,
            char* const argv[]
        );

        l: 新程序的命令参数以单独字符串指针的形式传入
            (const char* arg, ...),参数表以空指针结束。
            
        v: 新程序的命令参数以字符串指针数组的形式传入
            (char* const argv[]),数组以空指针结束。
            
        e: 新程序的环境变量以字符串指针数组的形式传入
            (char* const envp[]),数组以空指针结束,
            无 e 则从调用进程的 environ 变量中复制。
            
        p: 若第一个参数中不包含“/”,则将其视为文件名,
            根据 PATH 环境变量搜索该文件。

    范例:argenv.c(打印当前的所有命令行参数和环境变量)
        #include <stdio.h>
        void printarg (int argc, char* argv[]) {
        // 第一个参数是命令行参数个数,第二个参数是命令行参数数组
            printf ("---- 命令参数 ----\n");
            int i;
            for (i = 0; i < argc; i++)
                printf ("argv[%d] = %s\n", i, argv[i]);
            printf ("------------------\n");
        }
        void printenv (void) {
            printf ("---- 环境变量 ----\n");
            extern char** environ;
            char** env;
            for (env = environ; env && *env; env++)
                printf ("%s\n", *env);
            printf ("------------------\n");
        }
        int main (int argc, char* argv[]) {
            printarg (argc, argv);
            printenv ();
            return 0;
        }
        输入:
        # ./a.out hello word 1234 55.8
        输出:
        ---- 命令参数 ----
        argv[0] = ./a.out
        argv[1] = hello
        argv[2] = word
        argv[3] = 1234
        argv[4] = 55.8
        ---- 环境变量 ----
        省略 . . .
        -----------------
        
    范例:exec.c
        #include <stdio.h>
        #include <unistd.h>

        int main (void) {
            char* path = "./argenv";        // 调用上面的 argenv.c
            char* file = "argenv";
            char* argv[] = {path, "hello", "world", NULL};        // 参数,必须保证最后以 NULL 结尾;
            char* envp[] = {"USER=unknown", "PATH=/tmp", NULL};    // 环境,必须保证最后以 NULL 结尾;
            /*
            if (execl (path, argv[0], argv[1], argv[2], argv[3]) == -1) {
                perror ("execl");
                return -1;
            }
            输出:
            ---- 命令参数 ----
            argv[0] = ./argenv
            argv[1] = hello
            argv[2] = word
            ---- 环境变量 ----
            省略 . . .
            -----------------
            *//*
            if (execv (path, argv) == -1) {
                perror ("execv");
                return -1;
            }
            输出结果同上
            *//*
            if (execle (path, argv[0], argv[1], argv[2], argv[3], envp) == -1) {
                perror ("execle");
                return -1;
            }
            输出:
            ---- 命令参数 ----
            argv[0] = ./argenv
            argv[1] = hello
            argv[2] = word
            ---- 环境变量 ----
            USER=unknown
            PATH=/tmp
            -----------------
            *//*
            if (execve (path, argv, envp) == -1) {
                perror ("execve");
                return -1;
            }
            输出结果同上
            *//*
            if (execlp (file, argv[0], argv[1], argv[2], argv[3]) == -1) {
                perror ("execlp");
                return -1;
            }
            输出:
            ---- 命令参数 ----
            argv[0] = ./argenv
            argv[1] = hello
            argv[2] = word
            ---- 环境变量 ----
            省略 . . .
            -----------------
            (前提:PATH 必须包含当前路径,否则编译器无法找到 argenv.c,会报错;)
            */
            if (execvp (file, argv) == -1) {
                perror ("execvp");
                return -1;
            }
            /*
            输出:
            ---- 命令参数 ----
            argv[0] = ./argenv
            argv[1] = hello
            argv[2] = word
            ---- 环境变量 ----
            省略 . . .
            -----------------
            (前提:PATH 必须包含当前路径)
            */
            return 0;
        }
    
    范例:lsexe.c(实现 ls -l)
    思路:在 shell 里键入命令,shell 会创建一个子进程来执行命令,主进程调用控制台;
        #include <stdio.h>
        #include <unistd.h>
        int main (void) {
            if (execlp ("ls", "ls", "-l", NULL) == -1) {    
                perror ("execl");
                return -1;
            }
            return 0;
        }

    范例:fexe.c(实现 ls -l 和 ps -ef 功能)
        #include <stdio.h>
        #include <unistd.h>
        #include <sys/wait.h>
        int main (void) {
            // 创建子进程,如果不创建,那么只会执行 ls -l,而不会执行 ps -ef
            pid_t pid = vfork ();
            if (pid == -1) {
                perror ("fork");
                return -1;
            }
            // 实现 ls -l
            if (pid == 0) {
                if (execlp ("ls", "ls", "-l", NULL) == -1) {
                    perror ("execl");
                    return -1;
                }
                return 0;
            }
            // 回收子进程
            int status;
            if (waitpid (pid, &status, 0) == -1) {
                perror ("waitpid");
                return -1;
            }
            printf ("ls进程以%#x状态退出。\n", WEXITSTATUS (status));    // %#x 十六进制输出
            // 实现 ps -ef
            if ((pid = vfork ()) == -1) {
                perror ("vfork");
                return -1;
            }
            if (pid == 0) {
                char* argv[] = {"ps", "-ef", NULL};
                if (execvp ("ps", argv) == -1) {
                    perror ("execv");
                    return -1;
                }
                return 0;
            }
            if (waitpid (pid, &status, 0) == -1) {
                perror ("waitpid");
                return -1;
            }
            printf ("ps进程以%#x状态退出。\n", WEXITSTATUS (status));
            return 0;
        }

14    system

    #include <stdlib.h>
    int system (const char* command);

    1)标准 C 函数;执行 command,成功返回 command 对应进程的终止状态,失败返回 -1。
    2)若 command 取 NULL,返回非零表示 shell 可用,返回 0 表示 shell 不可用。

    3)该函数的实现,调用了 fork、exec 和 waitpid 等函数,其返回值:
        如果调用 fork 或 waitpid 函数出错,则返回 -1。
        如果调用 exec 函数出错,则在子进程中执行 exit(127)。
        如果都成功,则返回 command 对应进程的终止状态(由 waitpid 的 status 输出参数获得)。

    4)使用 system 函数而不用 fork+exec 的好处是,system 函数针对各种错误和信号都做了必要的处理。

    范例:system.c
        #include <stdio.h>
        #include <unistd.h>
        #include <sys/wait.h>
        int main (void) {
            int status;
            if ((status = system (NULL)) == -1) {
                perror ("system");
                return -1;
            }
            if (! status) {
                printf ("shell不可用!\n");
                return -1;
            }
            if ((status = system ("ls -l")) == -1) {
                perror ("system");
                return -1;
            }
            printf ("ls进程以%#x状态退出。\n", WEXITSTATUS (status));
            if ((status = system ("ps -ef")) == -1) {
                perror ("system");
                return -1;
            }
            printf ("ps进程以%#x状态退出。\n", WEXITSTATUS (status));
            return 0;
        }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值