进程控制

进程标识

  每个进程都有一个非负整形表示的惟一进程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),此进程负责支持虚拟存储器系统的分页操做。linux

#include <unistd.h>

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
//以上函数都无出错返回

fork

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

  由fork建立的新进程被称为子进程(child process)。fork函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值是新建子进程的进程ID。
  子进程和父进程继续执行fork调用以后IDE指令。子进程是父进程的副本。例如,子进程得到父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。父进程和子进程共享正文段。 因为在fork以后常常跟随着exec,因此如今的不少实现并不执行一个父进程数据段、站和堆的彻底副本。做为替代,使用了写时复制(Copy-On_Write,COW)技术。这些区域由父进程和子进程共享,并且内核将他们的访问权限改变为只读。若是父进程和子进程中任一个试图修改这些区域,则内核只为修改区域的那块内存制做一个副本,一般是虚拟存储系统中的一“页”。算法

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

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;
}

  能够看到当咱们将输出重定向到temp.out文件后多出个before fork的输出。write函数是不带缓存的。由于在fork以前调用write,因此其数据写到标准输出一次。可是标准IO是带缓存的。若是标准输出连到终端设备,则它是行缓存,不然它是全缓存。当以交互方式运行该程序时,只获得printf输出的行一次,其缘由是标准输出缓存由新行符刷新。当咱们将printf("before fork \n");后的换行符去掉以后即printf("before fork");来验证这一点,修改以后输出结果是:shell

  能够看到before fork打印了两次,这说明由于咱们去掉了换行符因此标准输出流的行缓存不会被flush。数组

  可是当将标注输出从新定向到一个文件时,却获得printf输出行两次。其缘由是,将标准输出从新定向到一个文件时标准输出流就不是行缓存而是全缓存了,在fork以前调用了printf一次,但当调用fork时,该行数据仍在缓存中,而后在父进程数据空间复制到子进程的过程当中时,该缓存数据也被复制到了子进程中。因而那时父、子进程各自有了带该行内容的缓存。在exit以前的第二个printf将其数据添加到现存的缓冲中。当每一个进程终止时,缓存中的内容将被写到相应文件中。缓存

文件共享

  对于上面的程序须要注意:在重定向父进程的标准输出时也重定向了子进程的标准输出。fork的一个特性是全部由父进程打开的文件描述符都被复制到子进程中。父、子进程每一个相同的打开文件描述符共享一个文件表项。
  这种共享文件的方式使父子进程对同一文件使用了一个文件位移量。对于如下状况:网络

  一个进程fork了一个子进程,而后等待子进程终止。假定,做为普通处理的一部分,父、子进程都向标准输出执行写操做。若是父进程使其标准输出重定向(极可能是由shell实现的),那么子进程写到该标准输出时,他将更新与父进程共享的该文件的位移量。在咱们所考虑的例子中,当父进程等待子进程时,子进程写到标准输出;而在子进程终止后,父进程也写入到标准输出上,而且知道其输出会添加在子进程所写数据以后。若是父、子进程不共享同一文件位移量,这种形式的交互就很难实现。app

  若是父、子进程写到同一文件描述符文件,但又没有任何形式的同步(例如使父进程等待子进程),那么它们的输出就会相互混合(假定所用的文件描述符是在fork以前打开的)。
  在fork以后处理文件描述符有两种常见的状况:ide

  1. 父进程等待子进程完成。这种状况下,父进程无需对其描述符作任何处理。
  2. 父、子进程各自执行不一样的程序段。在这种状况下,在fork以后,父、子进程各自它们不需使用的文件描述符,而且不干扰对方使用的文件描述符。

    除了打开文件以外,不少父进程的其余性质也会由子进程继承:函数

  • 实际用户ID、实际组ID、有效用户ID、有效组ID。
  • 添加组ID。
  • 进程组ID。
  • 对话期ID。
  • 控制终端。
  • 设置-用户-ID标志和设置-组-ID标志。
  • 当前工做目录。
  • 根目录。
  • 文件方式建立屏蔽字。(umask)
  • 信号屏蔽和排列。
  • 对任一打开文件描述符的在执行时关闭标志。
  • 环境。
  • 链接的共享存储段。

  父、子进程之间的区别是:测试

  1. fork的返回值。
  2. 进程ID
  3. 不一样的父进程iD。
  4. 子进程的tms_utime,tms_stime,tms_cutime以及tms_ustime设置为0。
  5. 父进程设置的锁,子进程不继承。
  6. 子进程的未决告警被清除。
  7. 子进程的未决信号集设置被清除。

使用fork失败的缘由主要有两个:

  1. 系统中已经有了太多的进程
  2. 该实际用户ID的进程总数超过了系统限制。

fork有如下两种用法:

  1. 一个父进程但愿复制本身,使父进程和子进程同时执行不一样的代码段。这在网络服务进程中是常见的–父进程等待客户端服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。
  2. 一个进程要执行一个不一样的程序。这对shell是常见的状况。在这种状况下,子进程从fork返回后当即调用exec。 

  有些操做系统将第二种用法的两个操做组合成一个操做,称为spawn。UNIX系统将这两个操做分开,由于在不少场合须要单独使用fork,其后并不更随exec。另外,将这两个操做分开,使得子进程在fork和exec之间能够更改本身的属性,如I/O重定向,用户ID、信号安排等。

vfork

  vfork函数的调用序列和返回值与fork相同,但二者的语义不一样。
  vfork函数用于建立一个新进程,而该进程的目的是exec一个新程序。vfork与fork同样都建立一个子进程,可是他并不将父进程的地址空间彻底复制到子进程中由于子进程会当即调用exec(或exit),因而也就不会引用该地址空间。不过在子进程调用exec或exit以前,他在父进程的空间中进行。这种优化工做方式在某些UNIX系统的实现中提升了效率,但若是子进程修改数据、进行函数调用、或者没有调用exec或exit就返回均可能带来未知的结果。
  vfork和fork之间的另外一个区别是:vfork保证子进程先运行,在它调用exec或exit以后父进程才可能被调度运行(若是在调用exec/exit以前子进程依赖于父进程的进一步动做,则会致使死锁)。当子进程调用这两个函数中的任意一个时,父进程会恢复运行。

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int glob = 6;
int main(void)
{
    int var;
    pid_t pid;

    var = 88;
    printf("before vfork\n");

    if ((pid = vfork()) < 0) {
        fprintf(stderr, "vfork error\n");
    } else if (pid == 0) {
        glob++;
        var++;
        _exit(0);
    }

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

    return 0;
}

  调用了_exit而不是exit。_exit并不执行IO缓存的刷新操做。若是是调用exit而不是_exit,则该程序的输出是:

  可见父进程的printf输出消失了。其缘由:子进程调用了exit,它刷新开关闭了全部标准IO流,这包括标准输出。虽然这是由子进程执行的,但倒是在父进程的地址空间中进行的,因此全部受到影响的标准IO FILE对象都是在父进程中。当父进程调用prinf时,标准输出已经被关闭了,因而printf返回-1。

  可是,在本身的linux系统上实验时,仍是有print输出。

  之因此结果不一样是由于在linux中子进程关闭的是本身的, 虽然他们共享标准输入、标准输出、标准出错等 “打开的文件”, 子进程exit时,也不过是递减一个引用计数,不可能关闭父进程的,因此父进程仍是有输出的。

 wait和waitpid

  当一个进程正常或异常终止时会向父进程发送SIGCHLD信号。对于这种信号系统默认会忽略。调用wait/waidpid的进程可能会:

  1. 阻塞(若是其子进程都还在运行);
  2. 当即返回子进程的终止状态(若是一个子进程已经终止正等待父进程存取其终止状态);
  3. 出错当即返回(若是它没有任何子进程);(若是进程因为收到SIGCHLD信号而调用wait,则可指望wait会当即返回。可是在任一时刻调用则进程可能阻塞)
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
//返回值: 成功返回进程ID, 出错-1.

这两个函数区别:

  • wait若是在子进程终止前调用则会阻塞,而waitpid有一选项可使调用者不阻塞。
  • waitpid并不等待第一个终止的子进程--它有多个选项,能够控制它所等待的进程。

  若是调用者阻塞并且它有多个子进程,则在其一个子进程终止时,wait就当即返回。由于wait返回子进程ID,因此调用者知道是哪一个子进程终止了。
  参数statloc是一个整型指针。若是statloc不是一个空指针,则终止状态就存放到它所指向的单元内。若是不关心终止状态则将statloc设为空指针。
  这两个函数返回的整型状态由实现定义。其中某些位表示退出状态(正常退出),其余位则指示信号编号(异常返回),有一位指示是否产生了一个core文件等等。POSIX.1规定终止状态用定义在<sys/wait.h>中的各个宏来查看。有三个互斥的宏可用来取得进程终止的缘由,它们的名字都已WIF开始。基于这三个宏中哪个值是真,就可选用其余宏(这三个宏以外的其余宏)来取得终止状态、信号编号等。

  wait是只要有一个子进程终止就返回,waitpid能够指定子进程等待。对于waitpid的pid参数:

  • pid == -1, 等待任一子进程。这时waitpid与wait等效。
  • pid > 0, 等待子进程ID为pid。
  • pid == 0, 等待其组ID等于调用进程的组ID的任一子进程。
  • pid < -1 等待其组ID等于pid的绝对值的任一子进程。

  对于wait,其惟一的出错是没有子进程(函数调用被一个信号中断,也可能返回另外一种出错)。对于waitpid, 若是指定的进程或进程组不存在,或者调用进程没有子进程都能出错。options参数使咱们能进一步控制waitpid的操做。此参数或者是0,或者是下表中常数的逐位或运算。

  1. WCONTINUED:若实现支持做业控制,那么由pid指定的任一子进程在中止后已经继续,但其状态还没有报告,则返回其状态(POSIX.1的XSI扩展)
  2. WNOHANG:没有已终止的子进程时,则waitpid不阻塞。此时其返回值为0
  3. WUNTRACED:若某实现支持做业控制,而由pid指定的任一子进程已处于中止状态,而且其状态自中止依赖还未报告过,则返回其状态,WIFSTOPPED宏肯定返回值是否对应于一个中止的子进程

waitid

#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
//返回值:成功返回0;出错,返回-1
  • 与waitpid类似,waitid容许一个进程指定要等待的子进程,但它使用两个单独的参数表示要等待的子进程所属的类型,而不是将此与进程ID或进程组ID组合成一个参数。
  • id参数的做用域idtype的值相关

idtype

P_PID 等待一特定进程,id包含要等待子进程的进程ID
P_PGID 等待一特定进程组的任一子进程;id包含要等待子进程的进程组ID
P_ALL 等待任一子进程;忽略id

options

  下面各标志的按位或运算,指示调用者关注哪些状态变化
  WCONTINUED、WEXITED、WSTOPPED这3个常量之一必须在options参数中指定

  1. WCONTINUED:等待一进程,它之前曾被中止,此后又已继续,但其状态还没有报告
  2. WEXITED:等待已退出的进程
  3. WNOHANG:如无可用的子进程退出状态,当即返回而非阻塞
  4. WNOWAIT:不破坏子进程退出状态。该子进程退出状态可由后续的wait、waitid、waitpid调用取得
  5. WSTOPPED:等待一进程,它已经中止,但其状态还没有报告

  Linux 3.2.0、Mac OS X 10.6.八、Solaris 10支持waitid。Mac OS X 10.6.8并无设置siginfo结构中的全部信息

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);
 
//返回值:成功返回进程ID。出错,返回-1

  大多数UNIX系统提供了另外两个函数wait3和wait4。这两个函数是从UNIX系统的BSD分支延袭下来的。它们提供的功能比POSIX.函数wait和waitpid、waitod所提供的分别要多一个,这与附加参数rusage有关。该参数要求内核返回由终止进程及其全部子进程使用的资源概况
  资源信息包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等

  有关细节请参阅getrusage(2)手册页。这些资源信息只包括终止子进程,并不包括处于中止状态的子进程(这种资源信息与7.11节中所述的资源限制不一样)

 竞态调件

  当多个进程都企图对某共享数据进行某种处理,而最后的结果又取决于进程运行的顺序,则咱们认为这发生了竞态条件(race condition)。若是在fork以后的某种逻辑显式或隐式地依赖于在fork以后是父进程先运行仍是子进程先运行,那么fork函数就会是竞态条件活跃的孽生地。
  若是一个进程但愿等待一个子进程终止,则它必须调用wait函数。若是一个进程要等待其父进程终止,则可以使用下列形式的循环:

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

  这种形式的循环(称为按期询问(polling))的问题是它浪费了CPU时间,由于调用者每隔1秒都被唤醒,而后进行条件测试。
  为了不竞态条件和按期询问,在多个进程之间须要有某种形式的信号机制。在UNIX中可使用信号机制,各类形式的进程间通讯(IPC)也可以使用。
  在父、子进程的关系中,经常有如下状况:在fork以后,父、子进程都有一些事情要作。例如:父进程可能以子进程ID更新日志文件中的一个记录,而子进程则可能要为父进程建立一个文件。在本例中,要求每一个进程在执行完它的一套初始化操做后要通知对方,而且在继续运行以前,要等待另外一方完成其初始化操做。这种状况能够描述为以下:

TELL_WAIT();

if ((pid = fork()) < 0) {
    err_sys("fork error");
} else if (pid == 0) {
    TELL_PARENT(getppid());
    WAIT_PARENT();
    exit(0);
}

TELL_CHILD(pid);
WAIT_CHILD();
exit(0);

exec

  当进程调用exec函数时,该进程彻底由新进程代换,而新程序则从其main函数开始执行。由于调用exec并不建立新进程,因此先后的进程ID不会改变(父子进程这种关系也不会变)。exec只是用另外一个程序替换了当前进程的正文、数据、堆和栈段。

#include <unistd.h>

int execl(const char *pathname, const char *arg0, ... /* (char *) 0 */);
int execv(const char *pathname, char *const 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[]);
//返回值:出错-1,若成功不返回

  这些函数之间的第一个区别是前四个取路径名做为参数,后两个取文件名做为参数。当制定filename做为参数时:

  • 若是filename中包含/,则就将其视为路径名。
  • 不然按PATH环境变量。

  若是excelp和execvp中的任意一个使用路径前缀中的一个找到了一个可执行文件,可是该文件不是机器可执行代码文件,则就认为该文件是一个shell脚本,因而试着调用/bin/sh,并以该filename做为shell的输入。
  第二个区别与参数表的传递有关(l 表示表(list),v 表示矢量(vector))。函数execl、execlp和execle要求将新程序的每一个命令行参数都说明为一个单独的参数。这种参数表以空指针结尾。另外三个函数execv,execvp,execve则应先构造一个指向个参数的指针数组,而后将该数组地址做为这三个函数的参数。
  最后一个区别与向新程序传递环境表相关。以 e 结尾的两个函数excele和exceve能够传递一个指向环境字符串指针数组的指针。其余四个函数则使用调用进程中的environ变量为新程序复制现存的环境。
  六个函数之间的区别:

  每一个系统对参数表和环境表的总长度都有一个限制。当使用shell的文件名扩充功能产生一个文件名表时,可能会收到此值的限制。例如,命令:

grep _POSIX_SOURCE /usr/include/*/*.h

  在某些系统上可能产生下列形式的shell错误:arg list too long

  执行exec后进程ID没改变。除此以外,执行新程序的进程还保持了原进程的下列特征:

  • 进程ID和父进程ID。
  • 实际用户ID和实际组ID。
  • 添加组ID。
  • 进程组ID。
  • 对话期ID。
  • 控制终端。
  • 闹钟尚余留的时间。
  • 当前工做目录。
  • 根目录。
  • 文件方式建立屏蔽字。
  • 文件锁。
  • 进程信号屏蔽。
  • 未决信号。
  • 资源限制。
  • tms_utime,tms_stime,tms_cutime以及tms_ustime值。

  对打开文件的处理与每一个描述符的exec关闭标志值有关。进程中每一个打开描述符都有一个exec关闭标志。若此标志设置,则在执行exec时关闭该文件描述符,不然该描述符仍打开。除非特意用fcntl设置了该标志,不然系统的默认操做是在exec后仍保持这种描述符打开。
  POSIX.1明确要求在exec时关闭打开目录流。这一般是由opendir函数实现的,它调用fcntl函数为对应于打开目录流的描述符设置exec关闭标志。
  在exec先后实际用户ID和实际组ID保持不变,而有效ID是否改变则取决于所执行程序的文件的设置-用户-ID位和设置-组-ID位是否设置。若是新程序的设置-用户-ID位已设置,则有效用户ID变成程序文件的全部者的ID,不然有效用户ID不变。对组ID的处理方式与此相同。

  在不少UNIX实现中,这六个函数只有一个execve是系统调用。另外5个是库函数

更改用户id和组id

  能够用setuid设置实际用户ID和有效用户ID。能够用setgid函数设置实际组ID和有效组ID。

#include <sys/types.h>
#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
返回值:成功为0,出错为-1

  有关改变用户ID的规则。

  • 若进程具备root特权,则setuid函数将实际用户ID、有效用户ID,以及保存的设置-用户-ID设置为uid。
  • 若进程没有root权限,可是uid等于实际用户ID或保存的设置-用户-ID,则setuid只将有效用用户ID设置为uid。不改变实际用户ID和保存的设置-用户-ID。
  • 若是上面两个条件都不知足,则errno设置为EPERM,并返回出错。

   在这里假定_POSIX+_SAVED_IDS为真。若是没有提供这种功能,则上面所说的关于保存的设置-用户-ID部分都无效。
  关于内核所维护的三个用户ID,还要注意如下:

  • 只有root用户能够修改实际用户ID。一般,实际用户ID是在用户登陆时,由login程序设置的,并且毫不会改变它。由于login进程是一个root进程,当它调用setuid时,设置全部三个用户ID。
  • 仅当对程序文件设置了设置-用户-ID位时,exec函数设置有效用户ID。任什么时候候均可以调用setuid,将有效用户ID设置为实际用户ID或保存的设置-用户-ID。天然,不能将有效用户ID设置为任一随机值。
  • 保存的设置-用户-ID是由exec从有效用户ID复制的。在exec按文件用户ID设置了有效用户ID后,即进行这种复制,并将此副本保存起来。

  下表列出了改变这三个用户ID的不一样方法

 setreuid和setregid

  4.3+BSD支持setregid函数,其功能是交换实际用户ID和有效用户ID的值。

#include <sys/types.h>
#include <unistd.h>
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);

  其做用是一个非特权用户总能交换实际用户ID和有效用户ID。这就容许一个设置-用户-ID程序转换成只具备用户的普通权限,之后又可再次切换回设置-用户-ID所获得大的额外权限。

seteuid和setegid

  这两个函数只更改有效用户ID和有效组ID。

#include <sys/types.h>
#include <unistd.h>
int seteuid(uid_t uid);
int setegid(gid_t gid);
//返回值: 成功为0,出错为-1

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

组ID

  以上所说明的一切都以相似方式适用于各个组ID,添加组ID不受setgid函数的影响。

解释器文件

  解释器文件就是linxu中的shell脚本。这种文件是文本文件,其起始行的形式是:

#! pathname [optional-argument]

  在感叹号和pathname之间的空格是可任选的。最多见的是如下列行开始:

#! /bin/sh

  pathname一般是个绝对路径名,对它不进行什么特殊的处理(不适用PATH进行路径搜索)。
  不少系统对解释器文件第一行有长度限制(32个字符)。这包括#!、pathname、可选参数以及空格数。

system

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

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;
}
View Code

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

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

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);
}
View Code

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

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

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));  
} 
View Code

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

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

  在主函数中能够调用该函数,主函数为:

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

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函数进行了各类出错处理以及各类信号处理

进程会计 

  大多数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时间、传输的字节数等)都由内核保存早进程表中,并在一个新进程被建立时初始化。进程终止时写一个会计记录。这产生进程终止时写一个会计记录。这产生两个后果。

  1. 咱们不能获取永远不终止的进程的会计记录。像init这样的进程在系统生命周期中一直在运行,并不产生会计记录。这也一样适合于内核守护进程,他们一般不会终止。
  2. 在会计文件中记录的顺序对应于进程终止的顺序,而不是他们启动的顺序。为了肯定启动顺序,须要读所有会计文件,并按照日历时间进行排序。 

  会计记录对应于进程而不是程序。在fork以后,内核为子进程初始化一个记录,而不是在一个新程序被执行时初始化。虽然exec并不建立一个新的会计记录,但相应记录中的命令名改变了,AFORK标志则被清除。这意味着,若是一个进程顺序执行了3个程序(A exec B、B exec C,最后是C exit),只会写一个进程会计记录。在该记录中的命令名对应于程序C,可是CPU时间是程序A、B、C之和。

用户标识

  任一进程均可以获得其实际用户ID和有效用户ID及组ID。可是,咱们有时候但愿找到运行该程序用户的登陆名。咱们能够调用getpwuid。可是若是一个用户有多个登陆名,这些登陆名又对应着同一个用户ID,又将如何呢?能够用getlogin函数能够获取登录此登陆名。

#include <unistd.h>
char *getlogin(void);
//返回值:若成功,返回指向登陆名字符串的指针;若出错,返回NULL 

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

进程调度

  进程能够经过调整nice值选择以更低优先级运行。只有特权进程容许提升调度权限。
  nice值越小,优先级越高。NZERO是系统默认的nice值。
  进程能够经过nice函数获取或者更改她的nice值。使用这个函数,进程只影响本身的nice值,不能影响任何其余进程的nice值。

#include <unistd.h>
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 <sys/resource.h>
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 <sys/resource.h>
int setpriority(int which, id_t who, int value);   
//返回值:若成功,返回0;若出错,返回-1

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

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

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);
    }
}
View Code

进程时间

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

#include <sys/times.h>
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 <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 *);
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));  
} 
View Code
相关文章
相关标签/搜索