UNIX环境高级编程 学习笔记 第八章 进程控制

进程ID是一非负整型,唯一标识一个进程,因此常用其作其他标识符的一部分以保证其唯一性,如使用进程ID创建唯一文件名。

进程ID可复用。大多UNIX实现采用延迟复用算法,使得新进程ID不同于最近终止进程的ID。

系统中有专用进程,ID为0的进程通常是调度进程,常被称为交换进程,该进程是内核的一部分,它不执行任何磁盘上的程序,因此也被称为系统进程。ID为1的进程通常是init进程,在自举过程结束时由内核调用,它的程序文件在较早的UNIX中是/etc/init,较新版本中是/sbin/init,它负责自举内核后启动一个UNIX系统,init进程通常读取与系统有关的初始化文件(/etc/rc*文件或/etc/inittab文件以及/etc/init.d目录中的文件),并将系统引导到一个状态(如多用户)。init进程不会停止,它是一个普通的用户进程,但它以root权限运行。

Mac OS X10.4中,launchd进程替代了init进程,并且扩展了功能。

每个UNIX系统都有它自己的一套提供操作系统服务的内核进程,如某些UNIX的ID为2的进程是页守护进程,负责支持虚拟存储器系统的分页操作。

返回进程标识符的函数:
在这里插入图片描述
以上函数没有出错返回。

进程可以调用以下函数创建一个新进程:
在这里插入图片描述
fork函数创建的新进程被称为子进程,fork被调用一次,但返回两次,子进程返回0,父进程返回值是新建的子进程的进程ID。

子进程和父进程继续执行fork之后的指令,子进程是父进程的副本,子进程会获得父进程数据空间、堆和栈的副本。父进程和子进程并不共享存储空间部分,但共享正文段(包含可执行代码的段)。

由于fork调用后常跟exec调用,所以现在很多实现调用fork后不复制父进程的数据段、堆和栈的完全副本给子进程,作为替代,使用了写时复制(COW,Copy-On-Write)技术,进程数据段、堆和栈由父进程和子进程共享,而且内核将它们的访问权限修改为只读,如果父进程或子进程中任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一页。

某些系统中,fork系统调用的变体:
1.Linux 3.2.0提供clone系统调用,它是fork函数的推广,允许调用者控制哪些部分由父进程和子进程共享。
2.FreeBSD 8.0提供rfork系统调用,它类似于Linux的clone。
3.Solaris提供两个线程库,一个用于POSIX线程,它fork出来的进程仅包含调用该fork的线程;一个用于Solaris线程,它fork出来的进程包含调用该fork的线程所在进程的所有线程。但在Solaris 10中,两个线程库fork创建的进程都仅包含调用线程的副本。Solaris也提供fork1函数,它创建的进程只复制调用线程,forkall函数创建的进程复制了进程中所有线程。

使用fork:

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
using namespace std;

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

int main() {
    int var;
    pid_t pid;

    var = 88;
    if (write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1) {    // STDOUT_FILENO定义在unistd.h中
        cout << "write error" << endl;
        exit(1);
    }
    printf("before fork\n");

    if ((pid = fork()) < 0) {
        cout << "fork error" << endl;
        exit(1);
    } else if (pid == 0) {    // 子进程中递增变量
        ++globvar;
        ++var;
    } else {
        sleep(2);
    }

    cout << "pid = " << getpid() << ", glob = " << globvar << ", var = " << var << endl;
    exit(0);
}

执行它:
在这里插入图片描述
一般,fork后子进程还是父进程先执行是不确定的,取决于内核调度算法。如要求父进程和子进程间相互同步,则要求使用某种进程间通信。上例程序使父进程休眠2s,使子进程先执行,但系统比较繁忙时2s不一定够。

写标准输出时,写的字节数为buf的长度-1,这是为了不输出最后的null字节。strlen返回结果不包含null字节长度,而sizeof计算包含终止null字节的长度,并且strlen是函数调用,而sizeof是一个运算符,而sizeof的运算对象buf的缓冲区已用已知字符串初始化,长度固定,在编译时就计算出了缓冲区长度。

    char ca[] = "aaaaaa";
    const char* cp = "aaaaaa";
    cout << sizeof(ca) << " " << sizeof(cp) << endl;    // 输出7 4,因为cp是指针,而ca是数组

write函数是不带缓冲的,因此,在fork函数前调用write,要write的数据写到标准输出一次。而printf函数是带缓冲的,如果标准输出连到终端设备,则它是行缓冲的,否则就是全缓冲的。交互方式运行程序时,只输出了一次before fork,这是因为终端的标准输出缓冲区由换行符冲洗;而当标准输出定向到文件时,会输出两次before fork,因为在调用fork前将数据写在了缓冲区,而调用fork时,子进程也获得了一份缓冲区数据,在进程终止时,缓冲区中内容都被自动冲洗,写到了文件中。

上例程序第二次运行时,在父进程的标准输出被重定向之后,子进程也继承了这一点,子进程的标准输出也被重定向了。fork函数将父进程中所有打开文件描述符都复制到子进程中,对每个文件描述符来说,相当于执行了dup函数,父进程子进程每个相同的打开描述符共享一个文件表项:
在这里插入图片描述
调用fork后父进程和子进程共享同一个文件偏移量,有两种情况:
1.让父进程等待子进程写完并终止,此时文件描述符的偏移量已经更新,然后父进程接着子进程写。
2.父进程和子进程各自执行不同程序段,调用fork后父进程和子进程关闭它们不需要的文件描述符,然后写各自需要写的文件描述符。在网络服务进程中经常用到此方法。

如果父进程和子进程在写同一文件表项时没有进行同步,那么它们的输出就会混合。

除了打开文件外,子进程还继承父进程的:
在这里插入图片描述
在这里插入图片描述
父进程和子进程区别:
在这里插入图片描述
调用fork失败原因:
1.系统中进程过多(通常意味着某方面出现了问题)。
2.该实际用户ID的进程总数超过了系统限制。(CHILD_MAX值)

fork函数用处:
1.使父进程和子进程同时执行不同代码段,如网络服务进程中,父进程等待客户端服务请求,请求到达时,父进程调用fork使子进程处理该请求,而父进程继续等待下一服务请求。
2.一个程序要执行另一程序,shell中较常见,此时子进程从fork函数返回后立即调用exec。某些操作系统将其组合为一个操作,称为spawn,但分开后比较灵活,其中间还能执行一些操作,如更改子进程的IO重定向、用户ID、信号安排等。

vfork函数的参数列表和返回值与fork函数相同,用于创建一个新进程,但vfork函数创建的新进程的目的是exec一个新程序。

有人认为vfork函数有瑕疵,BSD开发者在4.4BSD中删除了该函数,但4.4BSD派生的所有开放源码版本BSD又加入了该函数。在SUSv3中,vfork被标记为弃用的接口,在SUSv4中被完全删除。由于历史原因我们了解一下它,可移植的应用不应使用它。

函数vfork不将父进程的地址空间完全复制到子进程中,因为子进程要立即调用exec(或exit),但在子进程调用exec或exit之前,它在父进程的空间中运行,因此如果子进程修改数据(除了用于存放vfork函数返回值的变量)、调用函数、没有调用exec或exit就返回会带来未知的结果。

函数vfork保证子进程先运行,子进程调用exec或exit后父进程才被调度运行。如果在调用这两个函数前子进程依赖父进程的进一步动作,则会导致死锁。

使用vfork函数:

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
using namespace std;

int globvar = 6;

int main() {
    int var;
    pid_t pid;

    var = 88;

    printf("before fork\n");

    if ((pid = vfork()) < 0) {
        cout << "vfork error" << endl;
        exit(1);
    } else if (pid == 0) {    // 子进程中递增变量
        ++globvar;
        ++var;
        _exit(0);    // _exit函数定义在头文件unistd.h中,而exit和_Exit函数定义在stdlib.h头文件中
    }

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

由于调用了vfork,父进程就不用调用sleep等待子进程运行结束了,因为内核可以保证子进程调用exec或exit前,父进程处于休眠状态。

运行它:
在这里插入图片描述
子进程对变量递增1的操作也改变了父进程中的变量值,因为子进程在父进程的地址空间中运行。

以上程序中子进程调用的是_exit,它不执行标准IO的冲洗工作,如果调用的是exit,则程序输出不确定,它依赖于IO库的实现,可能看到输出没变化,或没有父进程的输出(流在子进程结束时被关闭了),使printf函数返回-1。在不关闭流的系统上,如果想看到printf函数返回-1的结果,可以在exit函数前加上fclose(stdout),为观察其效果,可用以下代码代替printf函数:

int i = printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), glob, var);
sprintf(buf, "%d\n", i);    // buf需要提前定义
write(STDOUT_FILENO, buf, strlen(buf));

以上代码假设IO库在关闭流时不会关闭与流相关联的文件描述符,如果IO库会关闭文件描述符,则需要提前使用dup函数复制标准输出文件描述符,然后使用新的标准输出文件描述符代替上例中的STDOUT_FILENO。

以上程序中,如果子进程调用exit,而exit函数的具体实现只冲洗标准IO流,则这样的操作与调用_exit的操作相同,如果exit函数的具体实现还关闭IO流,那么表示标准输出的FILE对象的相关存储区将被清0,因此当父进程调用输出时不会产生任何输出,printf将返回-1。但此时文件描述符STDOUT_FILENO仍有效,子进程得到的是父进程的文件描述符数组的副本(描述符数组中的相同描述符实际在内存中指向的是同一个文件表项)。

大多数exit函数的现代实现不再关闭流,因为进程终止时内核将关闭进程中已打开的所有文件描述符,库中关闭这些只增加了开销而不会带来任何益处。

五种正常终止程序的方法:
1.main中执行return语句,等效于调用exit。
2.调用exit函数,此函数由ISO C定义,其操作包括调用各终止处理程序,然后关闭标准IO流等,但不处理文件描述符、多进程和作业控制,所以这一定义对UNIX系统而言是不完整的。
3.调用_exit或_Exit函数,ISO C定义_Exit目的是为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法,是否对标准IO进行冲洗取决于具体实现。UNIX系统中_Exit和_exit是同义的,并不冲洗IO流。_exit函数由exit函数调用。_exit是由POSIX.1说明的。大多数UNIX系统中,exit函数是一个标准C库函数,而_exit是系统调用。
4.进程的最后一个线程在其启动例程中执行return语句,但该线程的返回值不用作进程的返回值,最后一个线程从启动例程中返回时,该进程以终止状态0返回。
5.进程的最后一个线程调用pthread_exit,就像4中那样,进程终止状态总是0,与传送给pthread_exit的参数无关。

三种异常终止程序的方法:
1.调用abort,它产生SIGABRT信号,这是2中的一种特例。
2.进程接收到某些信号,信号来源可以是进程自身(如调用abort)、其它进程、内核(如当进程引用地址空间外的存储单元、除0时,内核会向进程发信号)。
3.最后一个线程对取消请求作出响应,默认,取消以延迟方式发生:一个线程要求取消另一个线程,若干时间之后,目标线程终止。

不管进程怎样终止,最后都会执行内核中的一段代码,它会为进程关闭所有打开描述符,释放进程所用的存储等。

通知父进程自己是如何终止的方法:三个终止函数,可将其退出状态作为参数传递给函数。而在异常终止时,内核产生一个指示其异常终止原因的终止状态。而父进程都能用wait或waitpid函数取得其终止状态。

以上提到的退出状态是传递给三个终止函数的参数。在最后调用_exit时,内核将退出状态转换成终止状态。

调用fork时,生成子进程,子进程终止时将其终止状态返回给父进程,但如果此时父进程已被终止,此被终止的父进程的所有子进程的父进程都变为init进程,称这些进程由init进程收养,操作过程是,在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止的进程的子进程,如果是,将该进程的父进程ID改为1。

如果子进程在父进程之前终止,内核会为每个终止子进程保存一定量的信息,当父进程调用wait或waitpid时,可以得到这些信息,包括进程ID、终止状态、进程使用的CPU总量等。内核可以释放终止进程所有存储区、关闭其打开文件。一个已经终止、但其父进程尚未对其进行善后处理(获取终止子进程的信息、释放它仍占用的资源)的进程被称为僵死进程。ps命令将僵死进程状态打印为Z。

由init进程收养的进程终止时不会变成僵死进程,init进程在无论何时只要有子进程终止,就会调用wait取得其终止状态,防止系统中塞满僵死进程。

一个进程正常或异常终止时,内核向其父进程发送SIGCHLD信号,因为子进程终止是个异步事件(可在父进程的任何运行时发生),所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或提供一个信号发生时调用的函数(信号处理程序)。系统默认忽略SIGCHLD信号。

调用wait或waitpid的进程:
1.如果所有子进程都在运行,则阻塞。
2.如果一个子进程已经终止,正等待父进程获取其终止状态,则取得该子进程的终止状态后立即返回。
3.如果它没有子进程,出错返回。

如果进程由于收到SIGCHLD信号而调用wait,则我们期望wait函数会立即返回,而如果在随机时间点调用wait,进程可能被阻塞。
在这里插入图片描述
两函数区别如下:
1.一个子进程终止前,wait函数使其调用者阻塞,而waitpid函数有一选项,可使调用者不阻塞。
2.waitpid函数并不等待在其调用之后的第一个终止子进程,只等待进程ID为pid参数的进程。

一个进程有多个子进程时,wait函数在任一子进程终止时就可返回,并且wait函数的返回值就是该终止子进程的pid。

两函数的statloc参数是一个整型指针,如statloc参数不是空指针,则终止进程的终止状态就存放在它所指地址内,如不关心终止状态,可将该参数设为空指针。

两函数返回的整型状态字由具体实现定义,POSIX.1规定,终止状态可用头文件sys/wait.h中各个宏查看,有四个互斥宏可取得终止进程的原因:
在这里插入图片描述
打印进程中止状态:

#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

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 gennerated)" : "");
#else       
            "");
#endif
    } else if (WIFSTOPPED(status)) {
        printf("child stopped, signal number = %d\n", WSTOPSIG(status));
    }
}

int main() {
    pid_t pid;
    int status;

    if ((pid = fork()) < 0) {
        printf("fork error");
        exit(1);
    } else if (pid == 0) {
        exit(7);    // 子进程正常退出
    }

    if (wait(&status) != pid) {
        printf("wait error");
        exit(1);
    }
    pr_exit(status);

    if ((pid = fork()) < 0) {
        printf("fork error");
        exit(1);
    } else if (pid == 0) {
        abort();    // 子进程异常终止
    }

    if (wait(&status) != pid) {
        printf("wait error");
        exit(1);
    }
    pr_exit(status);

    if ((pid = fork()) < 0) {
        printf("fork error");
        exit(1);
    } else if (pid == 0) {
        status /= 0;    // 产生信号SIGFPE
    }

    if (wait(&status) != pid) {
        printf("wait error");
        exit(1);
    }
    pr_exit(status);
    
    exit(0);
}

很多平台支持WCOREDUMP宏,但如果代码中定义了_POSIX_C_SOURCE宏,有些平台就隐藏了WCOREDUMP宏的定义。

执行上述代码:
在这里插入图片描述
信号的编号可以使用WTERMSIG(status)打印。可以在signal.h头文件中验证SIGABRT值为6,SIGFPE值为8。

如果想等待特定的子进程结束,在没有waitpid函数之前,需要一直调用wait,把获得的子进程ID和终止状态保存起来,直到获取到的子进程ID与期望的相同。之后,如果再需要其他子进程的终止情况,先在已终止的进程列表中找,找不到再调用wait循环以上步骤。

函数waitpid的pid参数作用:
1.pid=-1,等待任一子进程,等价于wait函数。
2.pid>0,等待进程ID与pid相等的子进程。
3.pid=0,等待组ID等于调用该函数的进程的组ID的任一子进程。
4.pid<-1,等待组ID等于pid绝对值的任一子进程。

函数wait唯一出错可能是调用进程没有子进程(函数调用被另一信号中断时也可能出错),但对于waitpid函数,如果指定的进程或进程组不存在,或参数pid指定的进程不是调用进程的子进程,都可能出错。

waitpid函数的options参数使我们能进一步控制函数waitpid的操作,此参数或者是0,或者是下图常量按位或运算的结果:
在这里插入图片描述
FreeBSD 8.0和Solaris 10支持另一个非标准的可选常量WNOWAIT,它使系统把终止状态已经由waitpid函数返回的进程保持等待状态,它的终止状态可再次被wait或waitpid函数读取。

waitpid函数比wait函数多的功能:
1.waitpid函数可等待一个特定的进程。
2.waitpid函数提供了一个wait函数的非阻塞版本,有时我们想获取任意一个子进程状态,又不想阻塞。
3.waitpid函数通过WUNTRACED和WCONTINUED选项支持作业控制。

调用fork两次以避免僵死进程:

#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

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

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

    if (waitpid(pid, NULL, 0)) {
        printf("waitpid error\n");
        exit(1);
    }
    exit(0);
}

执行代码:
在这里插入图片描述
第二个子进程调用sleep尽可能保证其父进程(第一个子进程)在打印父进程ID时已终止,这样打印出来它的父进程ID就是init进程的ID了。

第一个子进程终止时,父进程waitpid函数会返回,之后父进程结束,shell会打印一次提示符,之后再是第二个子进程的输出。

SUS(它扩展了POSIX.1)有另一个取得进程中止状态的函数:
在这里插入图片描述
waitid函数允许一个进程指定要等待的子进程,但它使用两个单独的参数表示要等待的子进程所属的类型,而不是像waitpid函数一样将进程ID和进程组ID组合成一个参数。id参数的作用与idtype参数的值相关,该函数支持的idtype参数取值如下:
在这里插入图片描述
waitid函数的options参数是以下各标志的按位或运算:
在这里插入图片描述
上图中的WCONTINUED、WEXITED、WSTOPPED三个常量之一必须被指定。

infop参数是指向siginfo_t结构的指针,造成子进程状态改变的信号的详细信息通过此参数返回,siginfo_t结构如下:
在这里插入图片描述

Mac OS X 10.6.8并没有设置siginfo_t结构中的所有信息。

大多UNIX系统提供以下两个函数:
在这里插入图片描述
这两个函数是从BSD分支沿袭下来的,它通过一个附加参数,允许内核返回由终止进程及其所有子进程使用的资源概况。资源统计信息包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等,可以查询getrusage函数的手册页获得。

当多个进程都企图对共享数据处理,而最后的结果又取决于进程运行的顺序时,我们认为发生了竞争条件。

如果调用fork之后的某种逻辑显式或隐式地依赖于在调用fork之后是父进程先运行还是子进程先运行,此时fork函数就是竞争条件活跃的滋生地。

通常,我们不能预料哪个进程先运行,即使我们知道哪个进程先运行,在开始运行后发生的事情也取决于系统负载和内核调度算法。

如果一个进程希望等待一个子进程终止,则它必须调用wait函数中的一个,如果一个进程要等待其父进程终止,可使用下列循环:

while (getppid() != 1) {
    sleep(1);    // sleep函数定义在头文件unistd.h中
}

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

为了避免竞争条件和轮询,多个进程间可以使用信号机制。

以下程序使用fork函数创建了一个竞争条件:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

static void charatatime(char *);

int main() {
    pid_t pid;

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

    exit(0);
}

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

    setbuf(stdout, NULL);    // 关闭缓冲,向终端打印时不需要遇到换行或缓冲区满就能打印
    for (ptr = str; (c = *ptr++) != 0; ) {
        putc(c, stdout);
    }
}

以上程序中标准输出是不带缓冲的,每个字符输出都调用一次write。

执行以上代码:
在这里插入图片描述
可见输出由于进程调度顺序不同,每次的输出也不同,如想父进程先输出,可以使用自定义的TELL函数和WAIT函数完成进程同步:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

static void charatatime(char *);

int main() {
    pid_t pid;

    TELL_WAIT();    // 为TELL_xxx函数和WAIT_xxx函数准备前期工作

    if ((pid = fork()) < 0) {
        printf("fork error\n");
        exit(1);
    } else if (pid == 0) {
        WAIT_PARENT();    // 等待父进程
        charatatime("output from child\n");
    } else {
        charatatime("output from parent\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);
    }
}

运行以上程序可以使父进程先运行。如想让子进程先运行:

    else if (pid == 0) {
        charatatime("output from child\n");
        TELL_PARENT(getppid());    // 告知父进程使其运行
    } else {
        WAIT_CHILD();    // 等待子进程先运行完
        charatatime("output from parent\n");
    }

当进程调用一种exec时,该进程执行的程序的正文段、数据段、堆栈段完全替换为新程序,而新程序从新程序的main函数开始执行,调用exec并不创建新进程,所以调用前后进程ID不变。

七种exec函数:
在这里插入图片描述
前四个函数以路径名为参数,其后两个函数以文件名为参数,最后一个函数以文件描述符为参数。当指定文件名为参数时:
1.如果文件名中有/,则将其视为路径名。
2.否则搜索PATH环境变量表示的目录中的同名的可执行文件。

PATH环境变量中零长的目录或"."表示当前目录。出于安全性考虑,不要把当前目录放在PATH中。

如果execlp或execvp函数使用PATH环境变量中的目录项找到了一个可执行文件,但该文件不是由连接编辑器产生的机器可执行文件,就认为该文件是一个shell脚本,于是会试着调用/bin/sh并以该filename作为shell的输入。

fexecve函数不寻找可执行文件,而是通过调用进程指定的文件描述符调用文件,这样可以防止特权恶意用户在调用进程找到要调用的文件后,exec函数执行该文件前,替换可执行文件。

exec函数族中,函数名中的l的表示list,v的表示vector。函数execl、execlp、execle要求将新程序的每个命令行参数都说明为一个单独的参数,这种参数表以空指针结尾。而函数execv、execvp、execve、fexecve则应先构造一个指向各参数的指针数组,然后将该数组的地址作为其实参。

execl、execlp、execle三个函数的表示命令行参数的最后一个实参必须是空指针,如果用常量0表示一个空指针,则必须将它转换为一个字符指针,否则会将其解释为一个int参数,此时如果int与指针长度不同,那么exec函数的实际参数将会是错误的。

exec函数的名字中带e的函数(execle、execve、fexecve)可以传递一个指向环境字符串指针数组的指针,另外四个不带e的函数会使用调用进程的environ变量为新程序复制现有的环境。

exec函数中带p的表示该函数使用文件名作为参数,并用PATH环境变量寻找可执行文件。

参数表和环境表的总长度是有限制的,由ARG_MAX给出,在POSIX.1中至少为4096字节。使用shell的文件名扩充功能产生一个文件名列表时,可能受到此值限制:

grep getrlimit /usr/share/man/*/*

在某些系统上会报错:
在这里插入图片描述
为了摆脱对参数表长度的限制,可以使用xargs命令,将长参数表断开成几部分,为寻找系统手册页中的getrlimit,可以:

find /usr/share/man -type f -print | xargs grep getrlimit

如果系统手册页是压缩过的:

find /usr/share/man -type f -print | xargs bzgrep getrlimit

find命令的-type f选项只匹配普通文件;-print选项打印出每个符合条件的文件名,并在每个文件名后跟一个换行符,如果find命令没有指定任何选项,则-print选项是默认使用的。find命令把匹配到的文件传递给xargs命令,而xargs命令每次只获取一部分文件而不是全部。

调用exec后,进程ID不变,新程序从调用进程处继承了:
在这里插入图片描述
对于打开文件,若文件描述符的执行时关闭标志(FD_CLOEXEC)打开,则调用exec时关闭此文件描述符,否则描述符仍打开。

POSIX.1规定调用exec时,关闭打开目录流,这通常是通过opendir函数实现的,opendir函数会调用fcntl函数为打开目录流的描述符设置执行时关闭标志。opendir函数会在打开目录时设置FD_CLOEXEC标志,而open函数不会。

调用exec前后实际用户ID和实际组ID不变,但有效ID是否改变取决于所执行的程序文件的设置用户ID位和设置组ID位是否设置,如设置,则有效用户ID变成程序文件所有者ID,否则有效用户ID不变,有效组ID也类似。

很多UNIX实现中,七个exec函数中只有execve函数是内核的系统调用,另外六个是库函数,六个库函数最终都要调用该execve系统调用。七个函数关系:
在这里插入图片描述
fexecve函数使用/proc/self/fd目录中的文件(这些文件其实是/dev/fd下文件的软链接,都指向文件描述符相应的文件)把文件描述符参数转换成路径名,execve函数用该路径名去执行程序(FreeBSD 8.0和Linux 3.2.0中这样实现fexecve函数)。

使用:set autoindent可以使vi编辑器换行后保持上行的缩进,但这只对本次打开有效,若想永久设置,在/etc/virc配置文件中添加set autoindent即可。

使用exec函数:

#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

char *env_init[] = { "USER=unknown", "PATH=/tmp", NULL };

int main() {
    pid_t pid;

    if ((pid = fork()) < 0) {
        printf("fork error\n");
        exit(1);
    } else if (pid == 0) {
        if (execle("/home/sar/bin/echoall", "echoall", "myarg1", "MY ARG2", (char *)0, env_init) < 0) {
            printf("execle error\n");
            exit(1);
        }
    }

    if (waitpid(pid, NULL, 0) < 0) {
        printf("wait error\n");
        exit(1);
    }

    if ((pid = fork()) < 0) {
        printf("fork error\n");
        exit(1);
    } else if (pid == 0) {
        if (execlp("echoall", "echoall", "only 1 arg", (char *)0) < 0) {
            printf("execlp error\n");
            exit(1);
        }
    }

    exit(0);
}

上述代码的execlp函数能工作是因为/home/sar/bin目录被添加到了PATH变量中。

以上程序要执行的echoall程序:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    int j = 0;
    char **ptr;
    extern char **environ;

    for (j = 0; j < argc; ++j) {
        printf("argv[%d] : %s\n", j, argv[j]);
    }

    for (ptr = environ; *ptr != 0; ++ptr) {
        printf("%s\n", *ptr);
    }

    exit(0);
}

执行以上程序:
在这里插入图片描述
图片没有截取全部输出,下面还有很多环境变量值。第二个exec调用打印argv[0]之前,shell提示符就出现了,这是因为父进程先结束了,父进程并没有等待子进程结束。

argv[0]可以是任何值,但常常是程序名或程序的完全路径名。在执行shell前,login命令(用于登录系统或切换登录身份)执行shell时,会在argv[0]之前加一个/前缀,这向shell表明它是作为登录shell被调用的,登录shell会执行启动配置文件命令,而非登录shell不执行。

当程序需要访问当前不允许访问的资源时,需要更换具有合适访问权限的用户ID或组ID。

设计应用时,使用最小特权模型。

设置用户ID和组ID的函数:
在这里插入图片描述
以上函数更改用户ID的规则(同样适用于更改组ID):
1.若进程有root特权,则函数将实际用户ID、有效用户ID以及保存的设置用户ID设置为其参数。
2.若进程没有root特权,但uid等于实际用户ID或保存的设置用户ID。则函数只将有效用户ID改为其参数。
3.若以上两条件均不满足,则将errno设置为EPERM,并返回-1。

综上,这两个函数对于非root特权用户来说,只能修改有效用户ID,对于root特权用户来说,可将三个ID都设为参数表示的ID。

如果_POSIX_SAVED_IDS为假,则以上规则中关于保存的设置用户ID部分无效。在POSIX.1 2001中,保存的ID是强制性功能,而较早版本是可选的,可通过编译时测试常量_POSIX_SAVED_IDS或以_SC_SAVED_IDS参数运行时调用sysconf来弄清楚是否支持这一功能。

对于内核维护的三个用户ID:
1.只有root进程可以更改实际用户ID。实际用户ID是在用户登录时,由login程序设置的,因为login是一个root进程,当它调用setuid时,设置了三个uid。
2.只有当程序文件设置了设置用户ID位时,exec函数才设置有效用户ID,如果没有设置设置用户ID位,exec函数不会改变有效用户ID,而是维持其现有值。任何时候都能调用setuid将有效用户ID设置为实际用户ID或保存的设置用户ID,但不能将有效用户ID设置为其他值。
3.保存的设置用户ID是exec函数从父进程的有效用户ID复制而来的。如果设置了文件的设置用户ID位,则在exec函数根据文件的设置用户ID设置了进程的有效用户ID后,原来的有效用户ID的副本就被保存到保存的设置用户id了。

在这里插入图片描述
上图中当设置用户id位打开时,调用exec后保存的设置用户id是从有效用户ID复制,目的是在调用exec后恢复原来的有效用户id。

没有可移植的方法获得保存的设置用户ID的当前值。FreeBSD 8.0和Linux 3.2.0提供了getresuid和getresgid函数获得保存的设置用户ID和保存的设置组ID。

历史上,BSD支持setreuid函数,功能是交换实际用户ID和有效用户ID。
在这里插入图片描述
如果其中一个参数值为-1,则表示相应的ID保持不变。

非特权用户总能交换其实际用户ID和有效用户ID,这允许一个设置用户ID程序交换成用户的普通权限,以后又可再次交换回设置用户ID权限。POSIX.1引进了保存的设置用户ID特性后,函数规则加强成,一个非特权用户调用该函数时可将其有效用户ID值设置成保存的设置用户ID。

setreuid和setregid函数都是SUS的XSI扩展,所有UNIX系统实现都应对它们提供支持。

4.3BSD没有保存的设置用户ID的特性,而是使用setreuid和setregid函数来代替。使用能交换实际用户ID和有效用户ID特性的程序生成shell进程时,特权程序必须在exec之前先将实际用户设置为普通用户,否则由于exec前后进程实际用户ID不变,exec后的程序通过交换实际用户ID和有效用户ID会使其具有特权。保护措施是,进程调用exec前先将进程的有效用户ID和实际用户ID都设置为非root权限的用户ID。

POSIX.1使用以下函数改变有效用户ID和有效组ID:
在这里插入图片描述
一个非root用户可以将有效用户ID设置为实际用户ID或保存的设置用户ID。对于root用户,仅将有效用户ID设置成参数uid。

在这里插入图片描述
上图中对于用户ID的讨论也适用于组ID,附属组不受setgid、setregid、setegid函数的影响。

at程序用于调度将来某个时刻要运行的命令。Linux 3.2.0上安装的at程序的设置用户ID是daemon用户。而FreeBSD 8.0、Mac OS X 10.6.8以及Solaris 10上安装的at程序的设置用户ID是root用户。在Linux 3.2.0上,at程序是由atd守护进程运行的。在FreeBSD 8.0和Solaris 10上,at程序通过cron守护进程运行。在Mac OS 10.6.8上,at程序通过launchd守护进程运行。

保存的设置用户ID特性的用法的例子:为了防止at命令被欺骗而运行不被允许的命令或读、写没有访问权限的文件,以下步骤会发生:
1.假定at程序文件由root用户拥有,并且其设置用户ID位已设置。当我们运行at时:
在这里插入图片描述
2.at程序第一件事是降低特权,以用户特权运行。它调用setuid把有效用户ID设置为实际用户ID:
在这里插入图片描述
3.at命令以我们的用户特权运行,直到它需要访问配置文件,配置文件中控制哪条命令将要运行以及何时运行。这些文件由为我们运行at命令的守护进程拥有。at命令会调用seteuid将有效用户ID设为root(为我们运行at命令的守护进程的ID,此处是root),这能成功是因为保存的设置用户ID是root(这是为何我们需要保存的设置用户ID)。之后:
在这里插入图片描述
4.在修改了配置文件,更新了新的要运行的命令以及何时运行它之后,at命令调用seteuid把有效用户ID改为我们的用户ID,这阻止了特权的滥用:
在这里插入图片描述
5.守护进程开始以root权限运行,为了运行我们设定的命令,守护进程fork出一个子进程,并调用setuid将子进程的ID设置为我们的用户ID,由于子进程以root权限运行,因此setuid函数改变了所有ID:
在这里插入图片描述
现在守护进程可以安全地替我们执行程序。

所有现今的UNIX系统都支持解释器文件,这种文件是文本文件,它的存在文件的起始行:

#! pathname [optional-argument]

感叹号和pathname之间的空格可省略,常见的解释器文件以下列行开始:

#! /bin/sh

pathname常是绝对路径名,对它不会进行特殊处理(如使用PATH环境变量搜索)。对这种文件的识别是由内核作为执行exec系统调用的一部分完成的。内核实际执行的不是该解释器文件,而是pathname指定的文件。

很多系统对解释器文件第一行的长度有限制,这包括#!、pathname、可选参数、终止换行符和空格数。FreeBSD 8.0中,是4097字节。Linux 3.2.0中,是128字节。Mac OS X 10.6.8中是513字节。Solaris 10中是1024字节。

执行解释器文件的程序:

#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid;

    if ((pid = fork()) < 0) {
        printf("fork error\n");
	    exit(1);
    } else if (pid == 0) {
        if (execl("/root/Desktop/apue/ch7/interpreter/testinterp", "testinterp", "myarg1", "MY ARG2", (char *)0) < 0) {
		    printf("execl error\n");
		    exit(1);
		}
    }

    if (waitpid(pid, NULL, 0) < 0) {
        printf("waitpid error\n");
	    exit(1);
    }

    exit(0);
}

解释器文件内容:

#! /root/Desktop/apue/ch7/myEcho/echoarg foo

echoarg程序会输出所有参数,包括argv[0],执行以上程序:
在这里插入图片描述
argv[0]是解释器的pathname,argv[1]是解释器文件中的可选参数,接下来是execl函数的命令行参数,直接忽略第一个命令行参数,它相当于argv[0],此值可以是任何值,通常execl函数的第一个参数包含的信息比第二个参数(第二个参数是第一个命令行参数,即argv[0])更多,因此接下来输出的是execl函数的第一个参数,而非第二个参数testinterp。

如果以上程序中使用的是execlp函数而非execl函数时,如果execlp在同样的路径上搜索到文件testinterp,则结果是一样的,因为根据图8-15,execlp函数和execl函数最后都会调用execve。

解释器的pathname后可跟可选参数,如以下awkexample文件:

#! /usr/bin/awk -f
# Note: on Solaris, use nawk instead
BEGIN {
    for (i = 0; i < ARGC; i++)
        printf "ARGV[%d] = %s\n", i, ARGV[i]
    exit
}

执行它:
在这里插入图片描述
在执行/usr/bin/awk时,-f选项含义是从指定的文件中读取要执行的awk命令,假设awkexample文件位于目录/path下,以上执行过程相当于执行了:

/usr/bin/awk -f /path/awkexample file1 FILENAME2 f3

如上,解释器文件的路径名被传递给解释器awk,因为不能期望解释器会使用PATH定位该解释器文件,所以要将解释器文件的完整路径名传送给解释器。当awk读解释器文件时,因为#是awk的注释字符,所以它忽略第一行。

上例中的-f选项是必须的,如果没有-f:

/bin/awk /path/awkexample file1 FILENAME2 f3

awk会把字符串“/path/awkexample”解释为一个awk程序。

解释器文件使用户提高了效率,但代价是内核开销,因为识别解释器文件的是内核,解释器文件优点:
1.解释器文件可以隐藏用某种语言写的脚本,如运行上例程序时:

./awkexample optional-argument

并不需要知道以上程序实际是一个awk脚本,否则就要这样执行:

awk -f awkexample optional-argument

2.提高了效率,将awkexample放到某shell脚本中时:

awk 'BEGIN {
    for (i = 0; i < ARGC; i++)
        printf "ARGV[%d] = %s\n", i, ARGV[i]
    exit
}' $*

这会做更多工作,首先shell读此shell脚本名,然后试图以此shell脚本名为参数调用execlp,因为shell脚本是一个可执行文件,但不是机器可执行的,于是返回一个错误,execlp函数就认为该文件是一个shell脚本(实际就是),然后执行/bin/sh,并以该shell脚本的路径名作为参数,然后正确地执行该shell脚本,但为了运行awk,新shell还会调用fork、exec和wait,于是消耗了更多资源。
3.解释器脚本可以使我们用除/bin/sh之外的其他shell编写shell脚本,当execlp函数找到一个非机器可执行的可执行文件时,它总是调用/bin/sh来解释执行该文件,但用解释器脚本可以写成:

#! /bin/csh

如果shell和awk没有用#作为注释符,则上面所说的无效。

可用system函数在程序中执行一个命令字符串:
在这里插入图片描述
ISO C定义了system函数,但其操作对操作系统依赖性很强,POSIX.1包括了system接口,它扩展了ISO C的定义,描述了system在POSIX.1环境中的运行行为。

如果cdmstring参数是空指针,那么仅当命令处理程序可用时,system函数返回非零值,这样可以确定一个操作系统上是否支持system函数,UNIX中的system函数总是可用的。

system函数在其实现中调用了fork、exec、waitpid,因此有三种返回值:
1.fork失败或waitpid返回除EINTR(中断)之外的出错,则返回-1,并设置errno指示错误类型。
2.exec失败(表示不能执行shell),则其返回值如同shell执行了exit(127)。
3.三个函数都成功,system函数返回值是exec函数调用的shell的终止状态。

system函数的返回值可用测试wait函数的status参数的宏来测试。

没有对信号进行处理的system函数的一种实现:

#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.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;
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("usage: a.out cmdstring\n");
    }

    int res = system(argv[1]);

    exit(res);
}

shell的-c选项告诉shell程序取下一个命令行参数作为输入,而不是从标准输入或从一个给定的文件中读命令。shell对以null字节终止的命令字符串进行语法分析,将它们分成命令和参数。传递给shell的实际命令字符串可以包含任一有效的shell命令,如<>对输入输出重定向。

如果我们不使用shell执行,而是直接执行命令,会非常困难。直接执行命令时,我们希望使用execlp函数而非execl函数,execlp函数可以像shell那样使用PATH环境变量,但我们必须将以null字节终止的命令字符串分割成各个命令行参数,以便调用execlp。并且我们不能使用任何shell元字符(在shell中具有特殊意义的专用字符)。

我们调用的是_exit而非exit,这是为了防止标准IO缓冲(调用fork时由父进程复制到子进程中的)在子进程中被冲洗。

使用system函数而非fork和exec函数的原因是,system函数进行了各种出错处理和各种信号处理。

在早期UNIX系统中,没有waitpid函数,于是父进程使用以下形式的语句等待子进程结束:

while ((lastpid = wait(&status)) != pid && lastpid != -1) ;

但这样会使在特定进程终止之前终止的子进程的终止状态、进程ID被丢弃。

在一个设置了设置用户ID位的程序中调用system会有安全性漏洞:

#include <stdio.h>
#include <stdlib.h>

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

    if (argc < 2) {
        printf("command-line argument required\n");
	    exit(1);
    }

    if ((status = system(argv[1])) < 0) {
        printf("system error\n");
	    exit(1);
    }
    
    pr_exit(status);    // 打印system退出状态
    
    exit(0);
}

将以上程序编译为名为tsys的可执行文件。以下程序打印uid和euid:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    printf("real uid = %d, effective uid = %d\n", getuid(), geteuid());
    exit(0);
}

将以上程序编译为名为printuids的可执行文件,运行它们:
在这里插入图片描述
在这里插入图片描述
我们给予tsys程序的root特权在system函数调用fork和exec之后仍被保留。

有些实现通过更改/bin/sh,当有效用户ID和实际用户ID不同时,将有效用户ID改为实际用户ID,就可以避免该漏洞。

如果一个进程正以特殊权限(设置用户ID或设置组ID)运行,它又想调用fork产生另一个进程,那么它在调用fork后exec前,要改回普通权限。设置用户ID或设置组ID的程序不应使用system函数运行。

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

但任一标准都没对进程会计进行过说明,因此所有实现都有差别,如关于IO的数量,Solaris 10使用的单位是字节;FreeBSD 8.0和Mac OS X 10.6.8使用的单位是块,但同时又不考虑块长,这使得该计数值并无实际效用;Linux 3.2.0没有统计IO计数。并且每种实现都有自己的一套处理原始的会计数据的命令,如Solaris提供runacct和acctcom命令;FreeBSD提供sa命令处理并总结原始会计数据。

acct函数启用和禁用进程会计,唯一使用此函数的是accton命令,此命令是少数在几种平台上都类似的命令。root执行一个带路径名参数的accton命令启用会计处理,会计记录写到路径名参数指定的文件中,FreeBSD和Max OS X中,该文件通常是/var/account/acct;Linux中,该文件是/var/account/pacct;Solaris中,该文件是/var/adm/pacct。执行不带参数的accton命令会停止会计处理。

会计记录结构定义在头文件sys/acct.h中,每种系统的实现不相同,但会计记录结构基本如下:
在这里插入图片描述
在这里插入图片描述
大多数平台上,时间是以时钟滴答数记录的,但FreeBSD以微秒记录。

ac_flag成员记录了进程执行期间的某些事件:
在这里插入图片描述
会计记录所需的各个数据都由内核保存在进程表中,并在一个新进程被创建时(如fork函数创建时)初始化。

会计记录会在进程终止时写入(获取),这产生两个结果:
1.我们不能获取永远不终止的进程的会计记录。像init进程这样在系统生命周期中一直在运行的,不产生会计记录。还有内核守护进程,它们通常不会终止。
2.在会计文件中记录的顺序对应于进程终止的顺序,而非它们的启动顺序。为确定启动顺序,需要读全部会计文件,再按启动日历时间进行排序,但这不精确,因为日历时间的单位是秒,而墙上时钟时间的单位是时钟滴答(通常每秒时钟滴答数为60~128)。但我们仍不知道进程的终止时间。

会计记录对应于进程而非程序。fork后,内核为子进程初始化一个记录。exec时,不创建新会计记录,但相应记录中的命令名改变了,AFORK标志被清除。这意味着如果执行顺序是AexecB、BexecC、C最后exit,则只会有一个记录,并且记录中命令名是C,但CPU时间是三个程序总和。

以下程序产生会计数据,下图是代码的进程结构:
在这里插入图片描述

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>

int main() {
    pid_t pid;

    if ((pid = fork()) < 0) {
        printf("fork error\n");
		exit(1);
    } else if (pid != 0) {    // parent
        sleep(2);
	    exit(2);
    }

    if ((pid = fork()) < 0) {
        printf("fork error\n");
	    exit(1);
    } else if (pid != 0) {    // first child
        sleep(4);
	    abort();    // terminate with core dump
    }

    if ((pid = fork()) < 0) {
        printf("fork error\n");
	    exit(1);
    } else if (pid != 0) {    // second child
        execl("/bin/dd", "dd", "if=/etc/passwd", "of=/dev/null", NULL);
	    exit(7);
    }

    if ((pid = fork()) < 0) {   
        printf("fork error\n");
	    exit(1);
    } else if (pid != 0) {    // third child
        sleep(8);
	    exit(0);
    }

    sleep(6);    // fourth child
    kill(getpid(), SIGKILL);    // 被信号杀死,no core dump
    exit(6);
}

上图中dd命令作用为用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换,命令的参数如下:
1.if=文件名:输入文件名,默认为标准输入。即指定源文件。
2.of=文件名:输出文件名,默认为标准输出。即指定目的文件。

打印从系统会计文件中选出的字段:

#include <sys/acct.h>
#include <stdio.h>
#include <stdlib.h>

#if defined(BSD)    // different structure in FreeBSD,BSD系统上acct结构体中没有ac_stat成员,因此BSD上没有定义HAS_AC_STAT宏(表示不存在此成员)
	#define acct acctv2
	#define ac_flag ac_trailer.ac_flag
	#define FMT "%-*.*s e = %.0f, chars = %.0f, %c %c %c %c\n"    // 格式中的-表示左对齐,*.*中的第一个*表示输出的总宽度,第二个*表示输出的内容的宽度
#elif defined(HAS_AC_STAT)
	#define FMT "%-*.*s e = %6ld, chars = %.7ld, stat = %3u: %c %c %c %c\n"
#else
	#define FMT "%-*.*s e = %6ld, chars = %.7ld, %c %c %c %c\n"
#endif

#if defined(LINUX)
	#define acct acct_v3    // different structure in Linux
#endif

#if !defined(HAS_ACORE)
	#define ACORE 0
#endif

#if !defined(HAS_AXSIG)
	#define AXSIG 0
#endif

#if !defined(BSD)    // 如果不在BSD平台上,需要此函数将时间从表示时钟滴答数的comp_t转换为微秒
static unsigned long compt2ulong(comp_t comptime) {    // convert comp_t to unsigned long
    unsigned long val;
    int exp;

    val = comptime & 0x1fff;    // 13-bit fraction
    exp = (comptime >> 13) & 7;    // 3-bit exponent

    while (exp-- > 0) {
        val *= 8;
    }

    return val;
}
#endif

int main(int argc, char *argv[]) {
    struct acct acdata;
    FILE *fp;

    if (argc != 2) {
        printf("usage: pracct filename\n");
		exit(1);
    }
    if ((fp = fopen(argv[1], "r")) == NULL) {
        printf("can not open %s\n", argv[1]);
		exit(1);
    }
    while (fread(&acdata, sizeof(acdata), 1, fp) == 1) {    // 此处的fread函数读取1个单位的数据,每个单位长为sizeof(acdata),从文件流fp中读,将其存入acdata处,返回值为读取的单位元素的数量
        printf(FMT, (int)sizeof(acdata.ac_comm), (int)sizeof(acdata.ac_comm), acdata.ac_comm,
#if defined(BSD)    // BSD上的时间就是以微秒记录的
            acdata.ac_etime, acdata.ac_io,
#else    // 其他平台的时间以时钟滴答为单位,需要转换为微妙
            compt2ulong(acdata.ac_etime), compt2ulong(acdata.ac_io),
#endif

#if defined(HAS_AC_STAT)
            (unsigned char)acdata.ac_stat,
#endif
            acdata.ac_flag & ACORE ? 'D' : ' ',
            acdata.ac_flag & AXSIG ? 'x' : ' ',
            acdata.ac_flag & AFORK ? 'F' : ' ',
            acdata.ac_flag & ASU   ? 'D' : ' ');
    }

    if (ferror(fp)) {
        printf("read error\n");
    }

    exit(0);
}

BSD派生的平台不支持ac_stat成员(表示终止状态),支持该成员的平台上定义了HAS_AC_STAT宏,可通过测试此宏了解当前平台是否支持ac_stat成员。使用基于特性而非平台的符号使代码更易读且修改(增加新的基于特性的符号)容易。如果使用基于平台的符号:

// 如果不是在BSD或Mac OS上
#if !defined(BSD) && !defined(MAXOS) 

这样不知道要使用BSD和Mac OS都不支持的哪个功能。这样想将应用移植到一个同样不支持要使用的特定功能的平台上时,程序会出错。

同样定义了类似的符号常量以判断平台是否支持ACORE和AXSIG标志,我们不能直接使用它们,因为在Linux中它们被定义为enum类型,#ifdef中不能使用这种值,因此定义了相关的宏以便测试是否支持此标志。

#ifdef#if defined区别在于,前者只能判断一个宏是否存在(#ifdef XXX),而后者可以更加灵活(#if defined(XXX) && !defined(CCC) || DDD > 20),如果只判断一个宏是否存在,则两者效果相同。

为进行上述程序的测试,需要执行:
1.root权限下用accton命令启用会计处理。该命令结束时,会计处理已经启动,因此会计文件中的第一个记录来自此命令。
2.运行产生进程会计信息的程序。
3.root权限下关闭会计处理,在accton命令终止时进程会计已经停止,因此不会在进程会计文件中增加一项。
4.在Solaris上执行该程序,输出:
在这里插入图片描述
本系统每秒滴答数是100,而上图中调用sleep(2)的进程的休眠时间对应于墙上时钟时间202个时钟滴答数,而调用sleep(4)的进程的睡眠时间对应于墙上时钟时间420个时钟滴答数。一个进程的休眠时间并不精确,调用fork和exit也需要一些时间。

ac_stat成员并不是进程的真正终止状态,如果进程异常终止,则此成员包含的信息只是core标志位(一般是最高位)以及信号编号数(一般是低7位)。如果进程正常终止,则从会计文件中不能得到进程的退出(exit)状态。对于第一个子进程,此值是128+6,128是core标志位,6是系统信号SIGABRT(它是由调用abort函数产生的)的值。第四个子进程的终止状态值是9,它对应于信号SIGKILL的值。上图中不能分辨出父进程退出时所用的参数是2,第三个子进程退出时所用参数是0,这两个进程是正常终止的。

以上程序中,dd命令将文件/etc/passwd复制到第二个子进程中,该文件长度是777字节。而第二个子进程的IO字符数是此值的二倍多,其原因是读了777字节,又写了777字节,即使输出到空设备,仍对IO的字符数进行计算。dd命令还有31个附加字节,用于报告读写字节数的简要信息,它也会在stdout上打印。

除了调用execl的第二个子进程外,其他子进程都设置了F标志,这意味着它们都是fork出来的且没有使用exec函数。父进程没有F标志,这是因为执行父进程的交互式shell调用fork,然后使用exec函数执行了a.out文件。

第一个进程调用abort函数,产生信号SIGABRT,产生了core转储,但该进程的X(进程由信号杀死)和D(进程转储core)标志没有输出,这是因为Solaris不支持它们,其信息可从ac_stat成员导出。第四个进程也由信号终止,但SIGKILL信号不产生core转储,它只是终止进程。

第一个子进程虽然产生了core转储,但其IO字符数为0,原因是写core文件的IO不由该进程负责。

在口令文件中,一个用户ID可对应着多个登录项,它们的用户ID相同,但登录名不同(或登录shell不同),系统通常记录用户登录时使用的用户名,用getlogin函数可获取此登录名:
在这里插入图片描述
如果调用此函数的进程没有连接到用户登录所用的终端,则函数会失败,这些进程通常是守护进程。

获得了登录名,就可用getpwnam函数在口令文件中确定其登录shell。

为找到登录名,UNIX系统在历史上一直用ttyname函数获取终端名,再在utmp文件中找匹配项;FreeBSD和Mac OS X将登录名放在与进程表关联的会话结构中,并提供系统调用获取登录名。

System V提供cuserid函数返回登录名,此函数先调用getlogin函数,如失败再以getuid函数的返回值为参数调用getpwuid,此函数获取有效用户ID的用户名。POSIX.1的1990版本删除了cuserid函数。

环境变量LOGNAME通常由login程序以用户的登录名对其赋初值,并由登录shell继承,但用户可修改环境变量。

UNIX系统历史上对进程提供的只是基于调度优先级的粗粒度控制,调度策略和调度优先级是由内核确定的,进程可通过调整nice值选择以更低优先级运行,只有特权进程允许提高调度权限。

POSIX实时扩展增加了多个调度类别的接口以进一步细调行为,这些接口包含在POSIX.1的XSI扩展选项中。

SUS将nice值范围设为0~2 * NZERO - 1,有些实现支持0~2 * NZERO,nice值越小,优先级越高(越友好,调度优先级就越低)。NZERO是系统默认的nice值。定义NZERO的头文件因系统而异,除了头文件外,Linux 3.2.0可通过非标准的sysconf函数的参数_SC_NZERO来访问NZERO的值。

进程修改自己的nice值的函数:
在这里插入图片描述
incr参数被增加到调用进程的nice值上,如果incr参数太大,系统会直接将其降到最大的合法值,而不给出提示;如果incr太小,系统也会无声息地把它提高到最小合法值。

上图中返回值说明有误,nice的返回值是nice的变化值。-1是合法的成功返回值,因此返回-1时需要看errno来获知是否修改成功了,如果errno是0,说明修改成功了,如果非0,说明nice调用失败。

获取进程nice值,可获取其他进程的nice值:
在这里插入图片描述
which参数可取值:
1.PRIO_PROCESS:表示进程。
2.PRIO_PGRP:表示进程组。
3.PRIO_USER:表示用户ID。

根据which参数解释who参数,getpriority函数返回所有作用进程中优先级最高的nice值。当who为0时,根据which参数不同,作用进程为调用进程、调用进程所属进程组、进程的实际用户ID。

为进程、进程组中所有进程、某用户的所有进程设定优先级:
在这里插入图片描述
setpriority函数的which参数和who参数与getpriority函数中的同名参数含义相同。value参数会增加到NZERO上,变为新nice值。

nice系统调用起源于早期Research UNIX系统的PDP-11版本。getpriority和setpriority函数源于4.2BSD。

SUS没有对fork后子进程是否继承nice值制定规则,留给具体实现决定。遵循XSI的系统要求进程exec后保留nice值。

以下程序显示调整了nice值的效果,两个进程并行运行,各自增加自己的计数器,父进程使用默认nice值,子进程以可选命令参数指定的nice值运行,运行10s后,查看各自计数值:

#include <errno.h>
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#if defined(MACOS)
#include <sys/syslimits.h>
#elif defined(SOLARIS)
#include <limits.h>
#elif defined(BSD)
#include <sys/param.h>
#endif

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 = %lld\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 
#error NZERO undefined
// 处理到#error时停止编译,并输出用于自定义的错误信息
#endif
    printf("NZERO = %d\n", nzero);
    if (argc == 2) {
        adj = strtol(argv[1], NULL, 10);
    }
    gettimeofday(&end, NULL);
    end.tv_sec += 10;    // run for 10 seconds

    if ((pid = fork()) < 0) {
        printf("fork failed\n");
		exit(1);
    } else if (pid == 0) {    // child
        s = "child";
		printf("current nice value in child is %d, adjusting by %d\n", nzero, adj);
		errno = 0;
		if ((ret = nice(adj)) == -1 && errno != 0) {
		    printf("child set scheduling priority\n");
		    exit(1);
		}
		printf("now child nice value is %d\n", ret + nzero);
	    } else {    // parent
	        s = "parent";
			printf("current nice value in parent is %d\n", nzero);
	    }
	
	    for (; ; ) {
	        if (++count == 0) {
		    printf("%s counter wrap\n", s);
		    exit(1);
		}
		checktime(s);
    }
}

执行以上代码两次,一次用默认nice值,一次用最高有效nice值:
在这里插入图片描述
上图是在单处理器系统上运行的结果。如果系统有空闲资源(如多处理器系统或多核CPU,两个进程可能无需共享CPU),就无法看出nice值不同的进程的差异。

上图显示最低优先级时,父进程占98%的CPU,这依赖于不同UNIX系统的实现。

使用以下函数获取调用进程运行的墙上时钟时间、用户CPU时间、系统CPU时间:
在这里插入图片描述
tms结构如下,以上函数填写此结构:
在这里插入图片描述
tms结构中没有包含墙上时钟时间,times函数将墙上时钟时间作为返回值,此返回值是绝对时间,因此在计算墙上时钟时间时,需要调用两次times函数,两次的返回值相减就是墙上时钟时间(长期运行的进程其墙上时钟时间可能会溢出)。

tms结构中的两个针对子进程的字段tms_cutime、tms_cstime值含义为子进程使用的CPU时间,只有wait到的子进程的时间才会计算到这两个字段中。

clock_t类型含义为时钟滴答数,要想获得秒数,该值需要除每秒时钟滴答数(每秒时钟滴答数可用_SC_CLK_TCK为参数调用sysconf获得)。

大多实现提供了getrusage函数,它返回的值比times函数更多,此函数起源于BSD,因此BSD派生的实现比其他实现返回值字段更多。

以下程序读取命令行参数作为shell命令串执行,并对每个命令计时:

#include <sys/times.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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

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

    setbuf(stdout, NULL);
    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;

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

    if ((start = times(&tmsstart)) == -1) {    // starting values
        printf("times error\n");
		exit(1);
    }

    if ((status = system(cmd)) < 0) {    // execute command
        printf("system() error\n");
		exit(1);
    }

    if ((end = times(&tmsend)) == -1) {    // ending values
        printf("times error\n");
		exit(1);
    }

    pr_times(end - start, &tmsstart, &tmsend);
}

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\n");
		    exit(1);
		}
    }

    printf(" real: %7.2f\n", real / (double)clktck);
    printf(" user: %7.2f\n", (tmsend->tms_utime - tmsstart->tms_utime) / (double)clktck);
    printf(" sys:  %7.2f\n", (tmsend->tms_stime - tmsstart->tms_stime) / (double)clktck);
    printf(" before execute this cmd, tms_cutime: %7.2f, then after: %7.2f\n", tmsstart->tms_cutime / (double)clktck, tmsend->tms_cutime / (double)clktck);
    printf(" child user: %7.2f\n", (tmsend->tms_cutime - tmsstart->tms_cutime) / (double)clktck);
    printf(" child sys:  %7.2f\n", (tmsend->tms_cstime - tmsstart->tms_cstime) / (double)clktck);
}

运行它:
在这里插入图片描述
vfork函数产生的子进程与父进程共享地址空间,如果vfork函数在非main函数中被调用(如下程序),测试调用vfork的函数返回时会发生什么:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static void f1(void), f2(void);

int main() {
    f1();
    f2();
    _exit(0);
}

static void f1() {
    pid_t pid;

    if ((pid = vfork()) < 0) {
        printf("vfork error\n");
		exit(1);
    }

    // child and parent both return
}

static void f2() {
    char buf[1000];    // automatic variables
    int i;

    for (i = 0; i < sizeof(buf); ++i) {
        buf[i] = 0;
    }
}

以上是错误使用vfork函数的例子。main函数调用f1后,栈帧状态如下:
在这里插入图片描述
在函数f1中调用vfork后,父进程进入休眠状态,子进程从函数f1返回,接着子进程调用f2,且f2的栈帧覆盖了f1的栈帧。在函数f2中,子进程将栈中1000个字节都置为0,之后子进程从函数f2返回并调用_exit终止进程,父进程被唤醒,此时main栈帧以下的内容都被函数f2修改了,父进程从vfork函数调用后继续,并将从f1函数中返回,返回信息常保存在栈中,很可能被子进程修改了,因此父进程从函数f1返回时的结果依赖于系统的实现特征(如返回信息保存在栈帧中的具体位置、修改动态变量时覆盖了哪些信息等),父进程返回的结果通常是一个core文件。运行以上程序:
在这里插入图片描述
创建一个僵死进程,并用system函数调用ps命令验证:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

#ifdef SOLARIS
#define PSCMD "ps -a -o pid,ppid,s,tty,comm"
#else
#define PSCMD "ps -o pid,ppid,state,tty,command"
#endif

int main() {
    pid_t pid;

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

    // parent
    sleep(4);
    system(PSCMD);

    exit(0);
}

运行它:
在这里插入图片描述
如上,Z表示僵死进程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
UNIX环境高级编程笔记是关于在UNIX系统中进行高级编程的一些笔记和技巧的记录。这些笔记主要涉及文件I/O和进程管理等方面的内容。在UNIX系统中,文件I/O是通过文件描述符来进行操作的。文件描述符是一个整数,用来标识打开的文件。为了实现跨平台的兼容性,可以使用POSIX标准来进行文件操作。POSIX是一个操作系统接口的标准,它以UNIX为基础,但并不限于UNIX类系统。此外,Single UNIX Specification简称SUS,它是POSIX.1标准的一个超集,定义了UNIX系统的实现标准。在UNIX系统中,进程的初始化是由init进程来完成的。init进程会读取文件/etc/ttys,并根据其中定义的终端设备进行处理。对于每个允许登录的终端设备,init进程会调用fork函数生成一个子进程,并通过exec函数执行getty程序来处理该终端设备。通过这些技巧和方法,可以实现在UNIX环境下进行高级编程的需求。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [UNIX环境高级编程笔记](https://blog.csdn.net/qq_55537010/article/details/127837953)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [《UNIX环境高级编程学习笔记](https://blog.csdn.net/qq_42526420/article/details/123143423)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值