APUE第八章 进程控制

进程标识

每个进程都有一个非负整型标识的唯一进程ID(PID)。
虽然是唯一的,但是可以复用:当一个进程终止后,其PID就称为复用的候选者。
典型的进程:0进程,又称为swapper,是调度进程(内核进程)。
1进程,又称为init进程,在自举过程结束时由内核调用(用户进程,但是以超级用户特权运行)。孤儿进程的父进程就是init进程。

#include <unistd.h>
pid_t  getpid(void);
pid_t  getppid(void);
uid_t  getuid(void);
uid_t  geteuid(void);
gid_t  getgid(void);
gid_t  getegid(void);

函数fork

#include <unistd.h>
pid_t fork(void);
//子进程返回0,父进程返回子进程ID

子进程是父进程的副本。fork调用将父进程的数据空间、堆和栈复制到子进程的虚地址空间,上述区域并不共享,是直接拷贝。但是父进程和子进程共享正文段。
例:

int main(void){
    pid_t pid ;
    int a = 5;
    pid = fork();  //fork之后自子进程拷贝父进程副本

    if(pid == 0){
        printf("------------\n");
        printf("chld id = %d\n", getpid());
        a += 2;
        printf("a = %d\n", a);
        printf("------------\n");
        printf("\n");
    }
    else if (pid > 0){
        printf("------------\n");
        printf("father id = %d\n", getpid());
        a++;
        printf("a = %d\n", a);
        printf("------------\n");
        printf("\n");
    }
    //以下代码父进程、子进程都会执行一遍
    printf("------------\n");
    printf("id = %d\n", getpid());
    a++;
    printf("a = %d\n", a);
    printf("------------\n");
    printf("\n");
}
运行结果:
------------
father id = 2694
a = 6
------------

------------
id = 2694
a = 7
------------

------------
chld id = 2697
a = 7
------------

------------
id = 2697
a = 8
------------

文件共享

fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中,即是父进程和子进程每个相同的打开文件描述符共享一个文件表项,而且父子进程共享一个文件偏移量。

写时拷贝(copy-on-write)

Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。

vfork

vfork函数用于创建一个新进程,而该新进程的目的是exec一个新程序。类似于在fork返回后立即调用exec。
vfork会创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,是子进程直接共享父进程的整个地址空间,子进程完全运行在父进程地址空间上。这也就是说若在父进程中定义一个变量,在子进程中做修改的话,那么这个改变将会影响父进程中该变量的值,因为vfork的子进程直接运行在父进程的虚地址空间上。而fork则不然,因为fork将父进程数据段拷贝到了自己的虚地址空间,两者只共享正文段,而不共享数据段。

int main(void){
    pid_t pid ;
    int a = 5;
    pid = vfork();  //fork之后自子进程拷贝父进程副本

    if(pid == 0){
        printf("------------\n");
        printf("chld id = %d\n", getpid());
        a+=2;
        printf("a = %d\n", a);
        printf("------------\n");
        printf("\n");
        exit(0);
    }
    else if (pid > 0){
        sleep(2);
        printf("------------\n");
        printf("father id = %d\n", getpid());
        a++;
        printf("a = %d\n", a);
        printf("------------\n");
        printf("\n");
    }
    //以下代码父进程、子进程都会执行一遍
    printf("------------\n");
    printf("id = %d\n", getpid());
    a++;
    printf("a = %d\n", a);
    printf("------------\n");
    printf("\n");
}
执行结果:
------------
chld id = 3678
a = 7
------------

------------
father id = 3675
a = 8
------------

------------
id = 3675
a = 9
------------

从上例看出,vfork创建的子进程的确改变了父进程中的变量值,这是因为子进程运行在父进程的虚地址空间上。而且vork创建的子进程必须显示调用exit结束

函数exit

进程有8种终止方式,其中5种为正常终止方式:

  • 从main返回:在main函数内执行return语句,等效于调用exit
  • 调用exit:调用各种终止处理程序,关闭所有标准I/O流
  • 调用_exit或_Exit:为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法
  • 最后一个线程从其启动例程返回:进程以终止状态0返回
  • 从最后一个线程调用phread_exit

其中3种为异常终止方式:

  • 调用abort:产生SIGABRT信号
  • 接到一个信号:信号可由进程自身、其他进程或内核产生。例如,进程引用地址空间之外的存储单元、除以0,则内核将会为该进程产生相应的信号——》软中断
  • 最后一个线程对取消请求做出相应:一个线程要求取消另一个线程,若干时间之后,目标线程终止

    不管进程如何终止,最后都会执行内核中的同一段代码,这段代码为相应进程关闭所有打开的描述符,并释放它所使用的存储器。

子进程的终止

对上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。实现这一点的方法是将exit status作为exit、_exit或_Exit函数的参数,这样该进程的父进程就能够调用wait或waitpid函数取得其终止状态。

init进程对子进程收养

对于父进程已经终止的所有进程,它们的父进程都改变为init进程。在一个进程终止时,内核逐个检查所有活动进程,找到活动进程中这个正要终止的进程的子进程,并把这些子进程的父进程ID改为1。

僵死进程 (zombie)

在UNIX术语中,一个已经终止、但是其父进程尚未对其进行善后处理的进程被称为僵死进程。我理解为两种情况:

  • 子进程没有将退出状态发给父进程
  • 子进程将退出状态发给父进程,但父进程没有调用wait函数

由init收养的进程不可能成为僵死进程,因为init进程被编写为无论何时只要有一个自进程终止,init就会调用一个wait函数取得其终止状态。

函数wait和waitpid

wait函数族用来等来子进程状态的改变,若成立,则得到状态改变的子进程信息。状态改变包括:

  • 子进程终止
  • 子进程接收到信号而停止
  • 子进程接收到信号而恢复
    在子进程种子的情况下,调用wait将允许系统释放子进程的相关资源;若不调用wait将会使子进程成为僵死进程。
#include <sys/wait.h>

pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
//statloc指针指向终止进程的终止状态字

wait()

进程P1调用wait函数后,若P1没有子进程终止,则A将会一直阻塞在wait上直到有一个子进程终止。例如,P1调用wait,现在P1没有子进程终止,则P1阻塞;现在P1的子进程P2正常终止,内核向P1发送SIGCHLD信号,wait将会立即返回终止子进程的PID,因此父进程被唤醒从而知道子进程P2已经终止。此时系统将会释放P2的相关系统资源。

示例:

int main(void){

    pid_t pid;
    int   status;

    if((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0)
        exit(7);  //正常exit

    if (wait(&status) != pid)
        err_sys("wait error");
    pr_exit1(status);

    if((pid = fork()) < 0)
        err_sys("fork error");
    else if(pid == 0)
        abort();   //abort终止

    if (wait(&status) != pid)
        err_sys("wait error");
    pr_exit1(status);

    if((pid = fork()) < 0)
        err_sys("fork error");
    else if(pid == 0)
        status /= 0; //内核软中断信号


    if (wait(&status) != pid)
        err_sys("wait error");
    pr_exit1(status);

    exit(0);
}
运行结果
normal termination, exit status = 7 对应exit(0)
abnormal termination, signal number = 6 对应abort()
abnormal termination, signal number = 8 对应 status/=0

waitpid()

waitpid()可以等待一个特定的子进程。且waitpid提供了一个wait的非阻塞版本。

#include <sys/wait.h>
waitpid(pid_t pid, int *statloc, int options);

其中等待的特定进程由pid来指定,statloc仍然是指向退出状态的指针,options参数使我们能进一步控制waitpid的操作。

pid参数

  • pid == -1:等待任意子进程,这种情况下,waitpid和wait等效
  • pid > 0:等待进程ID与PID相等的子进程
  • pid == 0:等待组ID等于调用进程组ID的任一子进程
  • pid < -1:等待组ID等于PID绝对值的任一子进程

options参数:

  • WCONTINUED:若实现支持作业控制,那么由pid指定的任一子进程在停止后已经继续,但其状态尚未报告,则返回其状态。
  • WNOHANG:若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返回值为0.
  • WUNTRACED:若某实现支持作业控制,而由pid指定的任一子进程已处于停止状态,并且其状态自停止以来还未报告过,则返回其状态。

总结
waitpid函数提供了wait函数没有提供的三个功能

  1. waitpid可以等待一个特定的进程
  2. waitpid提供了一个wait的非阻塞版本,有时希望获取一个子进程的状态,但不想阻塞
  3. waitpid通过WUNTRACED和WCONTINUED选项支持作业控制

示例:用waitpid等待指定进程的状态改变

int main(void){
   pid_t pid;
   int status;

   if ((pid = fork()) < 0){

    err_sys("fork error");

   }else if(pid == 0){

    printf("i' m child process %d\n", getpid());
    int a = 2;
    sleep(2);
    abort();
  }

   if(waitpid(pid, &status, 0) != pid)
    err_sys("wait error");

   pr_exit(status);
}

函数waitid

取得进程终止状态函数waitid,也允许一个进程指定要等待的子进程。

#include <sys/wait.h>
int waitid(idtype_t idtpye, id_t id, siginfo_t *infop, int options);

infop参数是指向siginfo结构的指针,这意味着waitid等待子进程终止后,将由infop得到子进程状态改变相关信号信息。

函数wait3和wait4

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>

pid_t wait3(int *statloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);

竞争条件

示例:利用TELL_XXX()和WAIT_XXX()来完成父子进程同步。

static void charatatime(char *);

int main(void){

    pid_t pid;

    TELL_WAIT();

    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if(pid == 0){
        WAIT_PARENT();
        charatatime("output from child\n");
    }else {
        charatatime("output from father\n");
        TELL_CHILD(pid);
    }

    exit(0);
}

static void charatatime(char *str){
    char *ptr;
    int   c;

    setbuf(stdout, NULL);
    for(ptr = str; (c = *ptr++) != 0;)
        putc(c, stdout);

}

函数exec

用fork函数创建新的子进程之后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。调用exec并不创建新的进程,所以前后的进程ID并不改变。exec函数只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。
注:fork创建子进程后,父子进程是共享正文段,如果此时调用exec函数则使得子进程正文段的改变:即是用新程序的正文段、数据段和堆栈来填充子进程的虚地址空间(子进程的区)。

#include <unistd.h>
int execl(const char *pathname, const char *arg0, ...);
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0,...);
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ...);
int execvp(const char *filename, char *const argv[]);
int fexecvc(int fd, char *const argv[], char *const envp[]);

7个exec函数之间的区别:

  1. 前四个函数取路径名作为参数,后两个取文件名作为参数,最后一个则取文件描述符作为参数。
  2. execl、execlp、execle要求新程序的每个命令行参数都说明为exec函数的单独的一个参数。
  3. 以e结尾的三个函数参数中有char *const envp[],这是一个指向环境字符串指针数组的指针,即可以在执行exec函数时为新程序创建新的环境变量。其他四个函数则使用调用exec进程中的environ变量为新程序复制现有的环境。

环境变量实际上是一个个的环境字符串,环境字符串形式为
name = value.而环境表则是一个指针数组,数组的每个元素为指向一个环境字符串的指针。

在这七个函数中,只有execve是内核的系统调用,另外6个只是库函数,它们最终都要经过该系统调用。
在exec函数执行前后实际用户ID和实际组ID保持不变,而有效ID是否改变取决于所执行程序文件的设置用户ID位和设置组ID位是否设置。

更改用户ID和更改组ID

在UNIX系统中,特权(如能改变当前日期的表示法)以及访问控制(如能否读、写一个特定文件),是基于用户ID和组ID的。
在设计应用是,我们总是试图使用最小特权模型:使程序应当只具有为完成给定任务所需的最小特权。 可以降低安全性风险。

进程每次打开、创建或删除一个文件时,内核就进行文件访问权限测试,而这种测试可能涉及文件的所有者、进程的有效ID(有效用户ID和有效组ID)以及进程的附属组ID。内核进行的权限测试如下:

  1. 若进程的有效用户ID是0(超级用户),则允许访问
  2. 若进程的有效用户ID等于文件的所有者ID(也就是进程拥有此文件),则允许访问;否则拒绝访问
  3. 若进程的有效组ID或进程的附属组ID之一等于文件的组ID,如果组适当的访问权限位被设置,则允许访问;否拒绝访问。w+r
  4. 若其他用户o+r被设置,则允许访问

可以用setuid函数设置实际用户ID和有效用户ID,与此类似的是setgid函数。

#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);

注:关于内核所维护的3个用户ID:ruid、euid和suid

  1. 只有超级用户进程可以更待实际用户ID。通常实际用户ID是在用户登录时由login程序设置的,login是root进程,它调用setuid时将设置所有的3个用户ID
  2. 仅当对程序文件设置了设置用户ID(suid)位时,exec函数才设置有效用户ID,例如:
    当steve用户执行passwd命令的时候。Shell会fork出一个子进程,此时进程的EUID还是steve,然后exec程序/usr/bin/passwd。exec会根据/usr/bin/passwd的SUID位会把进程的EUID设成root, 此时这个进程都获得了root权限, 得到了读写/etc/shadow文件的权限, 从而steve用户可完成密码的修改。 exec退出后会恢复steve用户的EUID为steve.这样就不会使steve用户一直拥有root权限。

这就是我们设置用户ID位的作用,它的存在就是为了普通用户在某些需要特权权限时,去临时的改变有效用户ID而获得特权权限。

  1. 保存的设置用户ID是由exec复制有效用户ID得到的。
//用户交换实际用户ID和有效用户ID
#include <unistd.h>
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
//若任意参数值为-1,则表示相应的ID应当保持不变

一个非特权用户总能交换实际用户ID和有效用户ID。这就允许一个设置用户ID程序交换成普通用户权限,以后又可再次交换会设置用户ID权限。例如passwd程序中,exec函数可以根据/usr/bin/passwd将程序的SUID为交换为文件的root权限,passwd完毕后又将root权限交换回普通用户权限。

#include <unistd.h>
int seteuid(uid_t uid);
int setugid(gid_t gid);
//设置进程的有效用户ID和有效组ID

解释器文件

解释器是什么?
是执行解释型语言的程序。
对于编译型语言,是使用编译器将人类可读的代码转换为机器能够理解的「机器语言」文件,然后通过执行这个「机器语言」文件来实现程序的执行。
另一方面,对于解释型语言,是使用解释器将人类可读的代码逐行解释,一边解释一边执行这个程序。(这里的解释是将代码解释成机器语言,让计算机能够理解)

解释器文件的概念应该和编译型文件相对比来理解。比如.c文件通过gcc编译后可以生成可执行文件,我们便称这个.c文件为编译型文件。

区别
当执行解释器文件的时候,并不是执行这个文件本身,而是执行/bin/bash。
当执行编译型文件的时候,就是执行文件本身。
.c文件代表编译器文件一定要经过编译之后才能执行,而解释器文件则可以不用编译,因为在其执行时会直接调用/bin/sh再执行。

函数system

函数system可以在程序中嵌入shell命令,例如system(“date”),可以得到当前日历时间。实际上system(“date”)可以通过fork一个子进程然后再执行exec函数获取日历时间得到,但是system进行了所需的各种出错处理以及各种信号处理,保证shell命令的调用更加安全。
system函数的一种实现

#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>

int System(const char *cmdstring){

    pid_t   pid;
    int     status;

    if (cmdstring == NULL)
        return 1;

    if((pid = fork()) < 0)
        status = -1;
    else if(pid == 0){
        execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
        _exit(127);
    }
    else{
        while(waitpid(pid, &status, 0) < 0){
            if(errno != EINTR){
                status = -1;
                break;
            }
        }
    }
    return status;
}

设置用户ID程序不允许调用system函数,因为在设置用户ID程序中调用system函数后,可能会将有效用户ID修改为root(通过将setreuid()拷贝),然后程序退出后并不会修改有效用户ID,这意味着程序的root权限在system中执行了fork和exec之后仍然被保留了下来。因此应该直接使用fork和exec,而且在fork之后,exec之前要改回普通权限。

进程会计

大多数UNIX系统提供了一个选项以进行进程会计处理。启用该选项后,每当进程结束时内核就写一个会计记录。注意:

  • 永远不终止的进程的会计记录无法获取:init进程在系统生命周期内一直运行,因此不会产生会计记录。这意味进程终止才能触发写会计记录事件
  • 在会计文件中记录的顺序对应于进程终止的顺序,而不是它们启动的顺序。
  • 会计记录对应于进程而不是程序
typedef    u_short    comp_t;    /* 3-bit base 8 exponent; 13-bit fraction */

struct acct
{
    char    ac_flag;        /* flag */
    char    ac_stat;        /* termination status (signal & core flag only) */
                            /* (Solaris only) */
    uid_t    ac_uid;        /* real user ID */
    gid_t    ac_gid;        /* real group ID */
    dev_t    ac_tty;        /* controlling terminal */
    time_t   ac_btime;      /* starting calendar time */
    comp_t   ac_utime;      /* user CPU time (clock ticks) */
    comp_t   ac_stime;      /* system CPU time (clock ticks) */
    comp_t   ac_etime;      /* elapsed time (clock ticks) */
    comp_t   ac_mem;        /* average memory usage */
    comp_t   ac_io;         /* bytes transferred (by read and write) */
                            /* "blocks" on BSD system */
    comp_t   ac_rw;         /* blocks read or written */
                            /* (not present on BSD system) */
    char     ac_comm[8];    /* command name: [8] for Solaris, */
                            /* [10] for Mac OS X, [16] for FreeBSD, and */
                            /* [17] for Linux */

};

用户标识

#include <unistd.h>

char *getlogin(void);

可以调用该函数获得用户登录时登录名。

进程调度

进程时间

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值