UNIX环境高级编程学习之路(六)----进程控制

对于UNIX环境编程,工作中经常会用到相关知识,作为学习UNIX环境编程的经典书籍–UNIX环境高级编程,是每个UNIX编程人员必看的经典书籍之一,为了将相关知识重新进行学习,以系统的整合所学知识,遂以博文形式作为总结。

1、概述
本节介绍UNIX系统的进程控制,包括创建新进程、执行程序和进程终止。还将说明进程属性的各种ID–实际、有效和保存的用户ID和组ID,以及他们如何受到进程控制原语的影响。还包括了解释器文件和system函数。

2、进程标识
每一个进程都有一个非负整形表示的唯一进程ID。因为进程ID标识符总是唯一的,常将其用来作其他标识符的一部分以保证其唯一性。例如,应用程序有时就把进程ID作为名字的一部分来创建一个唯一的文件名。
虽然是唯一的,但是进程ID是可以复用的。当一个进程终止后,其进程ID就成为复用的候选者。大多数UNIX系统实现延迟复用算法,使得赋予新建进程的ID不同于最近终止进程所使用的的ID。
系统中有一些专用进程。ID为0的通常是调度进程,常常被称为交换进程(swapper)。该进程是内核的一部分,他并不执行任何磁盘上的程序,因此也被称为系统进程。进程ID 1通常是init进程,在自举过程结束时由内核调用。该进程的程序文件在UNIX的早期版本中是/etc/init,在较新的版本中是/sbin/init。此进程负责在在自举内核后启动一个UNIX系统。init通常读取与系统有关的初始化文件(/etc/rc*文件或者/etc/inittab文件,以及在/etc/init.d中的文件),并将系统引导到一个状态(如多用户)。init进程决不会终止。他是一个普通的用户进程,但是以超级用户特权运行。
每个UNIX系统实现都有他自己的一套提供操作系统服务的内核进程。例如,在某些UNIX的虚拟存储实现中,进程ID 2是页守护进程(page daemon),此进程负责支持虚拟存储器系统的分页操作。

#include 
pid_t getpid(void);
                        返回值:调用进程的进程ID
pid_t getppid(void);
                        返回值:调用进程的父进程ID
uid_t getuid(void);
                        返回值:调用进程的实际用户ID
uid_t geteuid(void);
                        返回值:调用进程的有效用户ID
gid_t getgid(void);
                        返回值:调用进程的实际组ID
gid_t getegid(void);
                        返回值:调用进程的有效组ID

这些函数都没有出错返回。

3、函数fork

一个现有的进程可以调用fork函数创建一个新进程。

#include 
pid_t fork(void);
返回值:子进程返回0,父进程返回子进程ID,若出错,返回-1

由fork创建的新进程被称为子进程(child process)。fork函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值是新建子进程的进程ID。
子进程和父进程继续执行fork调用之后IDE指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。父进程和子进程共享正文段。 由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、站和堆的完全副本。作为替代,使用了写时复制(Copy-On_Write,COW)技术。这些区域由父进程和子进程共享,而且内核将他们的访问权限改变为只读。如果父进程和子进程中任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一“页”。
以下程序演示了fork函数,从中看到子进程对变量所做的改变并不影响父进程中该变量的值。

#include 
#include 

int global_var = 6;
char buf[] = "a write to stdout\n";

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

    var = 88;
    if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
    {
            printf("write error!\n");
    }
    printf("before fork\n");

    if ((pid = fork()) < 0)
    {
        printf("fork error!\n");
    } else if (pid == 0) {
        global_var++;
        var++;
        printf("ppid = %ld\n", (long)getppid());
        } else {
        sleep(2);
        }

    printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), global_var, var);
    return 0;
}

执行此程序得到:


如上两图所示程序中fork与I/O函数之间的交互关系。write函数是不带缓冲的。因为在fork之前调用write,所以其数据写到标准输出一次。但是,标准I/O库是带缓冲的。当以交互方式运行该程序时,只得到该printf输出的行一次,其原因是标准输出缓冲区由换行符冲洗。但是当将标准输出重定向到一个文件时,却得到printf输出行两次。其原因是,==在fork之前调用了printf一次,但当调用fork时,该行数据仍在缓冲区中,然后再将父进程数据空间复制到子进程中时,该缓冲区数据也被复制到子进程中,此时父进程和子进程各有了带行内容的缓冲区。在exit之前的第二个printf将其数据追加到已有的缓冲区中==。当每个进程终止时,其缓冲区中的内容都被写到相应文件中。

freopen函数在一个指定的流上打开一个指定的文件,如若该流已经打开,则先关闭该流。若该流已经定向,则使用freopen清除该定向。此函数一般用于将一个指定文件打开为一个预定义的流:标准输入、标准输出和标准错误

进行重定向:

stdout = freopen("./tmp.out", "w", stdout);
  • 文件共享
    在重定向父进程的标准输出时,子进程的标准输出也被重定向。实际上,fork的一个特性是父进程的所有打开文件文件描述符都被复制到子进程中。我呢说“复制”是因为对每个文件描述符来说,就好像执行了dup函数。父进程和子进程每个相同的打开文件描述符共共享一个文件表项。
    考虑下列情况,一个进程具有3个不同的打开文件,他们是标准输入、标准输出和标准错误。在从fork返回时,有如下所示结构:

重要一点是,父进程和子进程共享同一文件偏移量。考虑下述情况:一个进程fork了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父进程和子进程都向标准输出进行写操作。如果父进程的标准输出已经重定向,那么子进程写到该标准输出时,它将更新与父进程共享的该文件的偏移量。当前例子中,当父进程等待子进程时,子进程写到标准输出:而在子进程终止后,父进程也写到标准输出上,并知道其输出会追加在子进程所写数据之后。如果子进程和父进程不共享同一文件偏移量,要实现这种形式的交互就困难的多,可能需要父进程显示的动作。
如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步(如父进程等待子进程),那么他们的输出就会相互混合。虽然这种情况是可能发生的,但并不是常用的操作模式。
在fork之后处理文件描述符有以下两种常见的情况。
(1)父进程等待子进程的完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已经做了相应的更新。
(2)父进程和子进程各自执行不同的程序段。在这种情况下,在fork之后,父进程和子进程各自关闭他们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。
父进程和子进程之间的区别如下。
* fork的返回值不同。
* 进程ID不同。
* 这两个进程的父进程ID不同:子进程的父进程ID是创建他的进程的ID,而父进程的父进程ID则不变。
* 子进程的tms_utime、tms_stime和tms_ustime的值设置为0。
* 子进程不继承父进程设置的文件锁。
* 子进程的未处理闹钟被清除。
* 子进程的未处理信号集设置为空集。

使用fork失败的原因主要有两个:(a)系统中已经有了太多的进程,(b)该实际用户ID的进程总数超过了系统限制。
fork有以下两种用法。
(1)一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的–父进程等待客户端服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。
(2)一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。
有些操作系统将第二种用法的两个操作组合成一个操作,称为spawn。UNIX系统将这两个操作分开,因为在很多场合需要单独使用fork,其后并不更随exec。另外,将这两个操作分开,使得子进程在fork和exec之间可以更改自己的属性,如I/O重定向,用户ID、信号安排等。

4、函数vfork
vfork函数的调用序列和返回值与fork相同,但两者的语义不同。
vfork函数用于创建一个新进程,而该进程的目的是exec一个新程序。vfork与fork一样都创建一个子进程,但是他并不将父进程的地址空间完全复制到子进程中因为子进程会立即调用exec(或exit),于是也就不会引用该地址空间。不过在子进程调用exec或exit之前,他在父进程的空间中进行。这种优化工作方式在某些UNIX系统的实现中提高了效率,但如果子进程修改数据、进行函数调用、或者没有调用exec或exit就返回都可能带来未知的结果。
vfork和fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。当子进程调用这两个函数中的任意一个时,父进程会恢复运行。
以下程序是用vfork代替fork后运行的代码。

#include 
#include 
#include 

int global_var = 6;

int main(void)
{
        int var;
        pid_t pid;
        var = 88;

        printf("before vfork\n");
        if ((pid = vfork()) < 0) {
                printf("vfork error!");
        } else if (pid == 0) {
                global_var++;
                var++;
                _exit(0);
        }

        printf("pid = %ld,glob = %d,var = %d\n", (long)getpid(), global_var, var);

        exit(0);
}

执行后结果为:

5、函数exit
进程有5种正常终止以及3种异常终止方式。5种正常终止方式如下:
(1)、在main函数内执行return语句。这等效于调用exit。
(2)、调用exit函数。其包括调用各终止处理程序(终止处理程序在调用atexit函数时登记),然后关闭所有标准I/O流等。因为ISO C并不处理文件描述符、多进程(父进程和子进程)以及作业控制,所以这一定义对于UNIX系统而言是不完整的。
(3)、调用_exit或_Exit函数。ISO C定义_Exit,其目的是为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法。对标准I/O是否进行冲洗,这取决于实现。在UNIX系统中,_Exit和_exit是同义的,并不冲洗标准I/O流。_exit函数由exit调用。他处理UNIX系统的特定的细节。
(4)、进程的最后一个线程在其启动例程中执行return语句。但是,该线程的返回值不用做进程的返回值。当最后一个线程从启动例程返回时,该进程以终止状态0返回。
(5)、进程的最后一个线程调用pthread_exit函数。在这种情况下,进程终止状态总是0,这与传递给pthread_exit的参数无关。
3种异常终止具体如下:
(1)调用abort。它产生SIGABR信号;
(2)当进程接收到某些信号时。信号可由进程自身、其它进程或内核产生。例如,若进程引用地址空间之外的存储单元、或者除以0,内核就会为该进程产生相应的信号。
(3)最后一个线程对“取消”请求作出响应。默认情况下,“取消”以延迟方式发生:一个线程要求取消另一个线程,若干时间之后,目标线程终止。
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放他所使用的存储器等。
对于上述任意一种终止情形,我们都希望终止进程能够通知他是如何终止的。对于3个终止函数(exit、_exit和_Exit),实现这一点的方法是,将其退出状态作为参数传递给函数。在异常终止情况,内核产生一个指示其异常终止的原因的终止状态。在任一一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。
在说明fork函数时,子进程是在父进程调用fork后生成的。子进程将其终止状态返回给父进程。但是如果父进程在子进程之前终止,该如何?回答是:对于父进程已经终止的所有进程,他们的父进程都改变为init进程。我们称这些进程由init进程收养。其操作过程大概是:在一个进程终止时,内核诸葛检查所有活动进程,以判断他是否是正要终止进程的子进程,如果是,则该进程的父进程的ID更改为1(init进程的ID)。这种处理方法保证了每一个进程都有一个父进程。
如果子进程在父进程之前终止,那么父进程如何在做相应检查时得到子进程的终止状态呢?如果子进程完全消失了,父进程在最终准备好检查子进程是否终止时是无法获得他的终止状态的。内核为每个终止子进程保存了一定量的信息。所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息。这些信息至少包括进程ID、该进程的终止状态以及该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。在UNIX术语中,一切已经终止、但是其父进程尚未对其进行善后处理的进程被称为僵尸进程。==如果一个长期运行的程序,它fork了很多子进程,那么除非父进程取得子进程的终止状态,不然子这些进程终止后就会变成僵尸进程==。
一个由init进程收养的进程最终终止时会发生什么?他会不会变成一个僵尸进程,不会的,因为init被编写成无论何时只要有一个子进程终止,init就会调用一个wait函数取得其终止状态。这样也就防止了在系统中塞满了僵尸进程。当提及“一个init的子进程”时,这指的是init直接产生的进程,也可能是其父进程已经终止,由init收养的过程。

6、函数wait和waitpid
当一个进程正常或者异常终止时,内核就向父进程发送SIGCHLD信号。由于子进程的终止是个异步事件,所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数。现在是调用wait或waitpid的进程可能会发生什么。
* 如果其所有子进程都还在运行,则阻塞。
* 如果一个子进程已经终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
* 如果他们有任何子进程,则立即出错返回。
如果进程由于接收到SIGCHLD信号而调用wait,我们期望wait会立即返回。但是如果在随机时间点调用wait,则进程有可能会阻塞。

#include 
pid_t wait(int *statloc);
pid_t waitpid(pid_t, int statloc, int options);
返回值:若成功,返回进程ID;若出错,返回0或-1

这两个函数的区别如下:
* 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一选项,可使调用者不阻塞。
* waitpid并不等待在其调用之后的第一个终止子进程,他有若干个选项,可以控制他所等待的进程。
如果子进程已经终止,并且是一个僵尸进程,则wait立即返回并取得该子进程的状态;否则wait使其调用者阻塞,直到使其调用者阻塞,直到一个子进程终止。如果调用者阻塞而且他有多个子进程,则在其某一子进程终止时,wait就立即返回。因为wait返回子进程的进程ID,所以它总能了解是哪一个子进程终止了。
这两个函数的参数statloc是一个整型指针。如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则将该参数指定为空指针。
终止状态用定义在

#include 
#include 
#include 
#include 

void pr_exit(int status)
{
    if (WIFEXITED(status))
    {
        printf("normal termination,exit status = %d\n", WEXITSTATUS(status));
    }
    else if (WIFSIGNALED(status))
    {
        printf("abnormal termination, signal numbet = %d\n",WTERMSIG(status));
    }
    else if (WIFSTOPPED(status)) 
    {
        printf("child stopped,signal number = %d\n",WSTOPSIG(status));
    }
}

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

    if ((pid = fork()) < 0)
        printf("fork error\n");
    else if (pid == 0) 
        exit(7);

    if (wait(&status) != pid)
        printf("wait error\n");
    pr_exit(status);

    if ((pid = fork()) < 0)
        printf("fork error\n");
    else if (pid == 0) 
        abort();

    if (wait(&status) != pid)
        printf("wait error\n");
    pr_exit(status);

    if ((pid = fork()) < 0)
        printf("fork error\n");
    else if (pid == 0) 
        status /= 0;

    if (wait(&status) != pid)
        printf("wait error\n");
    pr_exit(status);

    exit(0);
}

执行后输出为:

我们可以从WTERMSIG中打印信号编号。可以查看

#include 
#include 
#include 
#include 

int main(void)
{
    pid_t pid;

    if ((pid = fork()) < 0) {
        printf("fork error\n");
    } else if (pid == 0) {
        if ((pid = fork()) < 0) 
            printf("fork error\n");
        else if (pid > 0) 
            exit(0);

        sleep(2);
        printf("second child, parent pid = %ld", (long)getppid());
        exit(0);
    }

    if (waitpid(pid, NULL, 0) != pid)
        printf("waitpid error!\n");

    exit(0);
}

执行结果为:

fork两次相应的流程图为:

更多关于避免僵尸进程的资料,详见:
避免僵尸进程资料

7、函数waitid

另一个取得进程终止状态的函数为waitid,该函数类似于waitpid,但是提供了更多的灵活性。

#include 
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);   
返回值:成功,返回0;出错,返回-1

与waitpid类似,waitid允许一个进程指定要等待的子进程。但他使用两个单独的参数表示要等待的子进程所属的类型,而不是将此与进程ID或进程组ID组成一个参数。id参数的作用与idtype的值相关。

options参数是下图各标志的按位或运算。这些标志指示调用者关注哪些状态变化。

WCONTINUED,WEXITED或WSTOPPPED这三个常量之一必须在options参数中指定。

8、函数wait3和wait4
wait3和wait4函数提供的功能比函数wait、waitpid和waitid所提供的的功能多一个,这与附加参数有关系。该参数允许内核返回由终止进程及其所有子进程使用的资源概况。

#incline 
#include 
#include 
#include 

pid_t wait3(int *statloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);  
返回值:若成功,返回进程ID;若失败,返回-1   

资源统计信息包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等。

9、竞争条件
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,我们认为发生了竞争条件。如果在fork之后的某种逻辑显式或隐式的依赖于在fork之后是父进程先运行还是子进程先运行,那么fork函数就会是竞争条件活跃的滋生地。通常,我们不能预料哪一个进程先运行。即使我们知道哪一个进程先运行,在该进程开始运行后所发生的事情也依赖于系统负载以及内核的调度算法。
如果希望一个进程等待一个子进程终止,则她必须调wait函数中的一个。如果一个进程要等待其父进程终止,则可以使用下列形式的循环:

while (getppod() != 1)
    sleep(1);

这种形式的循环称为轮询,她的问题是浪费了CPU时间,因为调用者每隔1s都被唤醒,然后进行条件测试。

为了避免竞争条件和轮询,在多个进程之间需要某种形式的信号发送和接受的方法。在UNIX中可以使用信号机制,各种形式的IPC也可以使用。
在父进程和子进程的关系中,常常出现下述情况。在fork之后,父进程和子进程都有一些事情要做。例如,父进程可能要用子进程的ID更新日志文件中的一个记录,而子进程则可能要为父进程创建一个文件。再本例子中,要求每个进程执行完他的一套初始化操作后通知对方,并且在继续运行之前,在等待另一个完成其初始化操作。 这种情况下,可以用代码描述如下:

#include 
TELL_WAIT();
if ((pid = fork()) < 0)
    printf("fork error!\n");
else if (pid == o) {
    TELL_PARENT(getppid());
    WAIT_PARENT();
    exit (0);
}
TELL_CHILD(pid);
WAIT_CHILD();

exit (0);

以下例子输出两个字符串:一个由子进程输出,另一个由父进程输出,因为依赖于内核使用这两个进程运行的顺序级每个进程运行的时间长度,所以该进程包含了一个竞争条件。

#include 
#include 
#include 

static void charattime(char *);

int main(void)
{
    pid_t pid;

    if ((pid = fork()) < 0) {
        printf("fork error");
    } else if (pid == 0) {
        charattime("output from child\n");
    } else {
        charattime("output from parent\n");
    }
    exit(0);
}

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

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

执行以后的结果为:

修改以上程序,使其使用TELL和WAIT函数,于是形成了下面的程序。

#include 
#include 
#include 

static void charattime(char *);

int main(void)
{
    pid_t pid;

    TELL_WAIT();

    if ((pid = fork()) < 0) {
        printf("fork error");
    } else if (pid == 0) {
        WAIT_PARENT();
        charattime("output from child\n");
    } else {
        charattime("output from parent\n");
        TELL_CHILD(pid);
    }
    exit(0);
}

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

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

运行此程序能得到所预期的输出–两个进程的输出不在交叉混合。
以上程序是使父进程先运行。如果fork之后改成:

else if (pid == 0) {   
    charatatime("output from child\n");    
    TELL_PARENT(getppid());    
} else {  
    WAIT_CHILD();    
    charatatime("output from parent\n");
}

则子进程先运行。

10、函数exec

当进程调用一种fork函数创建新的子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。
==用fork可以创建新的进程,用exec可以执行新的程序。exit函数和wait函数处理终止和等待终止。这些是我们需要的基本的进程控制原语。==

#include 
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */) ;    
int execv(const char *pathname, const char *argv[]);   
int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */);   
int execve(const char *pathname, char *const argv[], char *const envp[]);   
int execlp(const char *filename, const char *arg0, ... /* (char *)0 */);   
int execvp(const char *filename, char *const argv[]);   
int fexecve(int fd, char *const argv[], char *const envp[]);   
返回值:若出错,返回-1,若成功,不返回

这些函数之间的第一个区别是前4个函数取路径名作为参数,后两个函数则取文件名作为参数,最后一个取文件描述符作为参数。当指定filename作为参数时:
* 如果filename中包含/,则就将其视为路径名;
* 否则就按PATH环境变量,在她说指定的各目录中搜寻可执行文件。
PATH变量包含了一张目录表,目录之间用冒号<:>分隔,例如,下列name=value环境字符串指定在4个目录中进行搜寻。
PATH=/bin:/usr/bin:/usr/local/bin:.
如果execlp或者execvp使用前缀中的一个找到了一个可执行文件,但是该文件不是由连接编辑器产生的可执行文件,则就认为该文件时一个shell脚本,于是试着调用/bin/sh,并以该filename作为shell的输入。
fexecve函数避免了寻找正确的可执行文件,而是依赖调用进程来完成这行工作。调用进程可以使用文件描述符验证所需要的文件并且无竞争的执行该文件。否则,拥有特权的恶意用户就可以在找到文件位置并且验证之后,但在调用进程执行该文件之前替换可执行文件。
第二个区别与参数表的传递有关(l表示list,v表示vector)。函数execl、execlp和execle要求新程序的每个命令行参数都说明为一个单独的参数。这种参数表以空指针结尾。另外4个函数(execv、execvp、execve和fexecve),应该先构造一个指向个参数的指针数组,然后将该数组地址作为这4个函数的参数。
最后一个区别与新程序传递环境表相关。以e结尾的3个函数(execle、execve和fexecve)可以传递一个指向环境字符串指针数组的指针。其他4个函数则使用调用进程中的environ变量为新程序复制现有的环境。
下图显示了这7个函数的区别

对打开文件测处理与每个描述符的执行时关闭标识符有关。在很多UNIX实现中,这7个函数中之有execve是内核的系统调用。另外6个只是库函数,它们最终都要调用该系统调用。这7个函数之间的关系如下图所示:

下面演示了exec函数的实例

#include 

#include 

int main()

{

    if(fork()==0){

        if(execlp("/usr/bin/env","env",NULL)<0)

        {

            perror("execlp error!");

            return -1 ;

        }

    }

    return 0 ;

}

执行后输出为:

11、更改用户ID和更改组ID
在UNIX系统中,特权(如能改变当前日期的表示法)以及访问控制(如能否读、写一个特定文件),是基于用户ID和组ID的。当程序需要增加特权,或需要访问当前并不允许访问的资源时,我们需要更换自己的用户ID或组ID,使得新ID具有合适的特权或者访问权限。与此类似,当程序需要降低其特权或阻止对某些资源的访问时,也需要更换用户ID或组ID,新ID不具有相同特权或访问这些资源的能力。
一般而言,在设计应用时,我们总是试图使用最小特权模型。依照此模型,我们的程序应当只具有为完成特定任务所需的最小特权。这降低了由恶意用户试图哄骗我们的程序以未预料的方式使用特权造成的安全性风险。
可以用setuid函数设置实际用户ID和有效用户ID。与此类似,可以用setgid函数设置实际组ID和有效组ID。

#include 
int setuid(uid_t uid);
int setgid(gid_t gid);
返回值:若成功,返回0;出错,返回-1

关于谁能修改ID有若干规则。
(1)若进程具有超级用户特权,则setuid函数将实际用户ID、有效用户ID以及保存的设置用户ID设置为uid。
(2)若进程没有超级用户特权,但是uid等于实际用户或保存的设置用户ID,则setuid只将有效用户ID设置为uid。不更改实际用户ID和保存的设置用户ID。
(3)如果上面的两个条件都不满足,则errno设置为EPERM,并返回-1。
关于内核维护的3个用户ID,注意下面几点。
(1)只有超级用户进程可以更改实际用户ID。通常,实际用户ID是在用户登陆时,由login程序设置的,而决不会改变它。因为login是一个超级用户进程,当它调用setuid时,设置所有3个用户ID。
(2)当对程序文件设置了设置用户ID位时,exec函数才设置有效用户ID。如果设置用户ID位没有设置,exec函数不会改变有效用户ID,从而维持其现有值。任何时候都可以调用setuid,将有效用户ID设置为实际用户ID或保存的设置用户ID。自然的,不能讲有效用户ID设置为任一随机值。
(3)保存的设置用户ID是由exec复制的有效用户ID而得到的。如果设置了文件的设置用户ID位,则在exec根据文件的用户ID设置了进程的有效用户ID以后,这个副本就被保存起来了。

具体可见以下博文:
设置用户ID位的那回事

  • 函数setreuid和setregid函数
    其功能是交换实际用户ID和有效用户ID的值。
#include 
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid); 
返回值:若成功,返回0,若出错,返回-1

如若其中任一参数的值为-1,则表示相应的ID位应当保持不变。
* 函数seteuid和setegid
只更改有效用户ID和有效组ID。

#include 
int seteuid(uid_t uid);
int setegid(gid_t gid);
返回值:若成功,返回0;若出错,返回-1   

一个非特权用户可将其有效用户ID设置为其实际用户ID或其保存的设置用户ID。对于一个特权用户则可将有效用户ID设置为uid。

12、解释器文件
所有现在的UNIX系统都支持解释器文件。这种文件是文本文件,其起始行的形式是:

! pathname [optional-argument]

在感叹号和pathname之间的空格是可选的。最常见的解释器文件是以下列行开始:

! /bin/sh

pathname是绝对路径名,对他不进行什么特殊处理(不适用PATHNAME进行路径搜索)。对这种文件的识别是由内核作为exec系统调用处理的一部分来完成的。内核使调用exec函数的进程实际执行的并不是该解释器文件,二是在该解释器文件第一行中pathname所指定的文件。一定要将解释器文件(文本文件)和解释器(由该解释器文件第一行中的pathname指定)区分开来。
很多系统对解释器文件第一行有长度限制。这包括#!、pathname、可选参数、终止换行符以及空格数。
exec执行普通文件和解释器文件的区别

主要分清两个区别:
(1)一般程序命令行执行和程序exec函数执行的区别;
(2)通过命令行执行解释器程序和程序exce函数执行解释器程序的区别。

13、函数system
在程序中执行一个命令字符串很方便。例如,假定要将时间和日期放到某一个文件中,则time得到当前日历时间,接着调用localtime将日历时间变换为年、月、日、时、分、秒的分解形式,然后调用strftime对上面的结果进行格式化处理,最后将结果写到文件中。但是通过调用system函数更容易做到这一点:

system("date > fiek");   
#include 
int system(const char *cmdstring);   

如果cmdstring是一个空指针,则仅当命令处理程序可用时,system返回非0,这一特征可以确定在一个给定的操作系统上是否支持system函数。在UNIX中,system总是可用的。
因为system函数在实现中调用了fork、exec和waitpid,因此有3种返回值。
(1)fork失败或者waitpid返回除EINTR之外的出错,则system返回-1,并且设置errno以指示错误类型。
(2)如果exec失败,则其返回值如同shell执行了exit。 (3)否则所有3个函数都成功,那么system的返回值是shell的终止状态,其格式在waitpid中说明。
一下是system的一种实现:

#include 
#include 
#include 
#include 

int system(const char *cmdstring)
{
    pid_t pid;
    int status;
    if (NULL == cmdstring)
    {
        return 1;
    }

    if ((pid = fork()) < 0)
    {
        status = -1;
    }
    else if (pid == 0) {
        execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
        //execl("/home/dang/WorkSpace/test","xxxxxx","yyyyyy",(char*)0);
        _exit(127);
    } 
    else 
    { 
        while ((waitpid(pid, &status, 0)) < 0) 
        {
            if (errno != EINTR)
            {
                status = -1;
                break;
            }
        }
        printf("parent process\n");
    }

    return status;
}

int main(int argc, char **argv)
{
    if (system("date > file")<0)
    {
        puts("cmd is failed");
    }
    else
    {
        puts("cmd is success");
    }

    puts("main done");
    return 0;
}

shell的-c选项告诉shell程序读取下一个命令行参数(在这里是cmdstring)作为命令输入。shell对以null字节终止的命令字符串进行语法分析,将他们分成命令行参数。传递给shell的实际命令字符串可以包括任一有效的shell命令。例如,可以用<和>岁输入和输出重定向。
以下程序对system函数进行测试。
(1)首先在调用system函数时,如果出错,则掉用exit函数退出,如下代码测试为:

#include 
#include 
#include 

int main(void)
{
    int status;
    if ((status = system("date")) < 0) 
    {
        printf("system error!\n");
    }

    exit(status);
    //pr_exit(status);

    if ((status = system("nosuchcommand")) < 0) 
    {
        printf("system error!\n");
    }

    exit(status);
    //pr_exit(status);

    if ((status = system("who;exit 44")) < 0) 
    {
        printf("system error!\n");
    }

    exit(status);
    //pr_exit(status);

    exit(0);
}

执行后,输出为:

在遇到第二个非法的system命令时,执行失败,进程退出。
(2)在调用system函数时,如果出错,则调用pr_exit函数,打印出出错原因以及状态,以下为测试程序:
pr_exit函数的实现为:

#include 
#include 

void pr_exit(int status)  
{  
        if (WIFEXITED(status))  
                printf("normal termination,exit status = %d\n", WEXITSTATUS(status));  
        else if (WIFSIGNALED(status))  
                printf("abnormal termination, signal number = %d%s\n", WTERMSIG(status),  
        #ifdef WCOREDUMP  
                WCOREDUMP(status) ? "(core file generated)" : "");  
        #else  
                "");  
        #endif  
        else if (WIFSTOPPED(status))  
                 printf("child stopped, signal number = %d\n", WSTOPSIG(status));  
}  

将该函数编译成共享库形式,执行命令:

gcc -O -fpic -shared -o pr_exit.so pr_exit.c

在主函数中可以调用该函数,主函数为:

#include 
#include 
#include 

int main(void)
{
    int status;
    if ((status = system("date")) < 0) 
    {
        printf("system error!\n");
    }

    pr_exit(status);

    if ((status = system("nosuchcommand")) < 0) 
    {
        printf("system error!\n");
    }

    pr_exit(status);

    if ((status = system("who;exit 44")) < 0) 
    {
        printf("system error!\n");
    }
    pr_exit(status);

    exit(0);
}

通过如下命令编译该程序:

gcc -o systemTest systemTest.c ./pr_exit.so 

执行还程序,得到如下结果:

使用system而不直接使用fork和exec的优点是:system函数进行了各种出错处理以及各种信号处理。

14、进程会计
大多数UNIX系统提供了一个选项以进行进程会计处理。启用该选项后,每个进程结束时内核就写一个会计记录。典型的会计记录包含总量较小的二进制数据,一般包括命令名、所使用的CPU时间总量、用户和组ID、启动时间等。
会计记录结构定义在头文件

typedef u_short comp_t;
struct acct
{
    char ac_flag;
    char ac_stat;
    uid_t ac_uid;
    gid_t ac_gid;
    dev_t ac_tty;
    time_t ac_btime;
    comp_t ac_utime;
    comp_t ac_stime;
    comp_t ac_etime;
    comp_t ac_mem;
    comp_t ac_io;

    comp_t ac_rw;
    char ac_comm[8];

};

会计记录所需的各个数据(各CPU时间、传输的字节数等)都由内核保存早进程表中,并在一个新进程被创建时初始化。进程终止时写一个会计记录。这产生进程终止时写一个会计记录。这产生两个后果。
第一、我们不能获取永远不终止的进程的会计记录。像init这样的进程在系统生命周期中一直在运行,并不产生会计记录。这也同样适合于内核守护进程,他们通常不会终止。
第二、在会计文件中记录的顺序对应于进程终止的顺序,而不是他们启动的顺序。为了确定启动顺序,需要读全部会计文件,并按照日历时间进行排序。
会计记录对应于进程而不是程序。在fork之后,内核为子进程初始化一个记录,而不是在一个新程序被执行时初始化。虽然exec并不创建一个新的会计记录,但相应记录中的命令名改变了,AFORK标志则被清除。这意味着,如果一个进程顺序执行了3个程序(A exec B、B exec C,最后是C exit),只会写一个进程会计记录。在该记录中的命令名对应于程序C,但是CPU时间是程序A、B、C之和。

15、用户标识
任一进程都可以得到其实际用户ID和有效用户ID及组ID。但是,我们有时候希望找到运行该程序用户的登录名。我们可以调用getpwuid。但是如果一个用户有多个登录名,这些登录名又对应着同一个用户ID,又将如何呢?可以用getlogin函数可以获取登陆此登录名。

#include 
char *getlogin(void);
返回值:若成功,返回指向登录名字符串的指针;若出错,返回NULL   

如果调用此函数的进程没有连接到用户登录时所用的终端,则函数会失败。通常称这些进程为守护进程(daemon)。
给出了登录名,就可用getpwnam在口令文件中查找用户的相应记录,从而确定其登录shell等。

16、进程调度
进程可以通过调整nice值选择以更低优先级运行。只有特权进程允许提高调度权限。
nice值越小,优先级越高。NZERO是系统默认的nice值。
进程可以通过nice函数获取或者更改她的nice值。使用这个函数,进程只影响自己的nice值,不能影响任何其他进程的nice值。

#include 
int nice(int incr);
返回值:若成功,返回信的nice值NZERO;若出错,返回-1

incr参数被增加到调用进程的nice值。如果incr太大,系统直接把他降到最大合法值,不给出提示。类似的,如果incr太小,系统也会无声息的把他提高到最小合法值。由于-1是合法的成功返回值,在调用nice函数之前需要清楚errno,在nice函数返回-1 时,需要检查他的值。如果nice调用成功,并且返回值为-1,那么errno任然为0.如果errno不为0,说明nice调用失败。
getpriority函数可以像nice函数那样用于获取进程的nice值,但是getpriority还可以获取一组相关进程的nice值。

#include 
int getpriority(int which ,id_t who);   
返回值:若成功,返回-NZERO~NZERO之间的nice值,若出错返回-1

which参数可以取下面三个值之一:PRIO_PROCESS表示进程,PRIO_PGRP表示进程组,PRIO_USER表示用户ID。which参数控制who参数是如何解释的,who参数选择感兴趣的一个或者多个进程。如果who参数为0,表示调用进程、进程组或者用户(取决于which参数的值)。当which设为PRIO_USER并who为0时,使用调用进程的实际用户ID。如果which参数作用于多个进程,则返回所有进程中优先级最高的。
setpriority函数可以用于为进程、进程组和属于特定用户ID的所有进程设置优先级。

#include 
int setpriority(int which, id_t who, int value);   
返回值:若成功,返回0;若出错,返回-1

参数which和who与getpriority相同。value增加到NZERO上,然后变为新的nice值。
以下的程序度量了调整nice值的效果。两个进程并行运行,各自增加自己的计数器。

#include 
#include 
#include 
#include 
#include 

unsigned long long count;
struct timeval end;

void checktime(char *str)
{
    struct timeval tv;

    gettimeofday(&tv, NULL);
    if (tv.tv_sec >= end.tv_sec && tv.tv_usec >= end.tv_usec)
    {
        printf("%s count = %llu\n", str,count);
        exit(0);
    }
}

int main(int argc, char **argv)
{
    pid_t pid;
    char *s;
    int nzero, ret;
    int adj = 0;

    setbuf(stdout, NULL);
#if defined(NZERO)
    nzero = NZERO;
#elif defined(_SC_NZERO)
    nzero = sysconf(_SC_NZERO);
#else 
    nzero = 0;
//#error NZERO undefined    //编译器原因报错
#endif
    printf("NZERO = %d\n",nzero);
    if (argc == 2)
        adj = strtol(argv[1], NULL, 10);
    gettimeofday(&end, NULL);
    end.tv_sec += 10;

    if ((pid = fork()) < 0) {
        printf("fork error!\n");
    }
    else if (pid == 0) {
        s = "child";
        printf("current nice value in child is %d,adjusting by %d\n", nice(0) + nzero, adj) ;
        errno = 0;
        if ((ret = nice(adj)) == -1 && errno != 0)
        {
            printf("child set schduling priority\n");
            printf("now child nice value in parent is %d\n", nice(0) + nzero);
        }
    }
    else {
        s = "parent";
        printf("current nice value in parent is %d\n",nice(0) + nzero);
    }

    for (; ;)
    {
        if (++count == 0)
        {
            printf("%s counter wrap", s);
        }
        checktime(s);
    }
}

执行结果为: 

/************************************************ 
<< ./a.out 
NZERO = 20 
current nice in parent is 20 
current nice value in child is 20, adjusting by 0 
now child nice value is 20 
parent count = 258526410 
child count = 258645486 
<< ./a.out 20 
O = 20 
current nice in parent is 20 
current nice value in child is 20, adjusting by 20 
now child nice value is 39 
parent count = 507471598 
child count = 7639840 
************************************************/  

17、进程时间
任一进程都可以调用times函数获取它自己以及终止子进程的墙上时钟时间、用户CPU时间和系统CPU时间。

#include 
clock_t times(struct tms *buf);
返回值:若成功,返回流逝的墙上时钟时间;若出错,返回-1

此函数填写由buf指向的tms结构,该结构定义如下:

struct tms {   
    clock_t tms_utime;   
    clock_t tms_stime;
    clock_t tms_cutime;
    clock_t tms_cstime;
};

此结构没有包含墙上的时钟时间。times函数返回墙上时钟时间作为其函数值。此值是相对于过去的某一时刻度量的,所以不能用其绝对值而必须使用其相对值。例如,调用times,保存其返回值。在以后的某个时间再次调用times,从新返回的值减去以前返回的值,此差值就是墙上时钟时间。
所有由此函数返回的clock_t值都用_SC_CLK_TCK(由sysconf函数返回的每秒时钟滴答数)转换成秒数。

下面程序将每个命令行参数作为shell命令串执行,对每个命令计时,并打印从tms结构取得的值。

#include 
#include 
#include 
#include 

static void  pr_times(clock_t, struct tms *, struct tms *);
static void  do_cmd(char *);
static void  pr_exit(int );

int
main(int argc, char *argv[])
{
    int     i;

    for (i = 1; i < argc; i++)
        do_cmd(argv[i]);    /* once for each command-line arg */
    exit(0);
}
static void
do_cmd(char *cmd)       /* execute and time the "cmd" */
{
    struct tms  tmsstart, tmsend;
    clock_t     start, end;
    int         status;

    fprintf(stderr, "\ncommand: %s\n", cmd);

    if ( (start = times(&tmsstart)) == -1)  /* starting values */
        printf("times error");

    if ( (status = system(cmd)) < 0)        /* execute command */
        printf("system() error");

    if ( (end = times(&tmsend)) == -1)      /* ending values */
        printf("times error");

    pr_times(end-start, &tmsstart, &tmsend);
    pr_exit(status);
}
static void
pr_times(clock_t real, struct tms *tmsstart, struct tms *tmsend)
{
    static long     clktck = 0;

    if (clktck == 0)    /* fetch clock ticks per second first time */
        if ( (clktck = sysconf(_SC_CLK_TCK)) < 0)
            printf("sysconf error");
    fprintf(stderr, "  real:  %7.2f\n", real / (double) clktck);
    fprintf(stderr, "  user:  %7.2f\n",
            (tmsend->tms_utime - tmsstart->tms_utime) / (double) clktck);
    fprintf(stderr, "  sys:   %7.2f\n",
            (tmsend->tms_stime - tmsstart->tms_stime) / (double) clktck);
    fprintf(stderr, "  child user:  %7.2f\n",
            (tmsend->tms_cutime - tmsstart->tms_cutime) / (double) clktck);
    fprintf(stderr, "  child sys:   %7.2f\n",
            (tmsend->tms_cstime - tmsstart->tms_cstime) / (double) clktck);
}

static void 
pr_exit(int status)  
{  
        if (WIFEXITED(status))  
                printf("normal termination,exit status = %d\n", WEXITSTATUS(status));  
        else if (WIFSIGNALED(status))  
                printf("abnormal termination, signal number = %d%s\n", WTERMSIG(status),  
        #ifdef WCOREDUMP  
                WCOREDUMP(status) ? "(core file generated)" : "");  
        #else  
                "");  
        #endif  
        else if (WIFSTOPPED(status))  
                 printf("child stopped, signal number = %d\n", WSTOPSIG(status));  
}  

执行程序后,结果为:

————————————————华丽的风格线————————————————
QQ群:西安C/C++开发者,诚邀您的加入

你可能感兴趣的:(Linux)