UNIX进程控制

Posted 吉吉boy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UNIX进程控制相关的知识,希望对你有一定的参考价值。

UNIX编程第8章

进程标识:每个进程都有一个非负整型表示的唯一进程ID。唯一性。不过进程ID是可复用的,当一个进程终止后,其进程ID就成为复用的候选者。大多数UNIX系统采用延迟复用算法。

系统中有一些专用进程。ID为0的进程通常是调度进程,也被称作交换进程(swapper),该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程。

进程ID为1的通常是init进程,在自举过程结束时由内核调用。此进程负责在自举内核后启动一个UNIX系统。init通常读取与系统有关的初始化文件,并将系统引导到一个状态。init进程不会终止,它是一个普通的用户进程(与交换进程不同,它不是内核中的系统进程),但是它以超级用户特权运行。init通常会成为孤儿进程的父进程。

在某些UNIX的虚拟存储器实现中,进程ID 2是页守护进程(page daemon),此进程负责支持虚拟存储器系统的分页操作。

#include<unistd.h>

pid_t getpid(void);

  返回调用进程的进程ID;

pid_t getppid(void);

  返回调用进程的父进程ID;

pid_t getuid(void);

  返回调用进程的实际用户ID;

pid_t geteuid(void);

  返回调用进程的有效用户ID;

pid_t getgid(void);

  返回调用进程的实际组id;

pid_t getegid(void);

  返回调用进程的有效组ID;

这些函数没有出错返回。

 

函数fork::

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

#include<unistd.h>

pid_t fork(void);

  返回值,子进程返回0,父进程返回子进程ID,若出错,返回-1.

由fork创建的新进程被称为子进程。fork函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,父进程的返回值是fork新建子进程的进程ID。因为一个子进程可以获得其父进程及其本身的进程ID,但父进程不能通过函数获取子进程ID,所以父进程保留下fork返回的子进程ID。且进程ID为0的是内核交换进程,fork出来的真实进程ID不会是0.

子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本(是副本不是共享)。父进程和子进程共享正文段。

由于在fork之后会接着执行exec,所以很多实现并不执行一个父进程数据段、栈和堆的完全副本。作为代替,使用了写时复制技术(copy-on-write)。这些区域先由父进程和子进程共享,而且内核将他们的访问权限改变为只读。如果父进程或子进程其一试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟系统中的一“页”。

一般来说,在fork之后是父进程还是子进程先执行是不确定的,取决于具体的内核调度算法。

PS. strlen计算不包含终止null字节的字符串长度,而sizeof则计算包括终止null字节的缓冲区长度。而两者的另一个区别是,使用strlen需要进行一次函数调用,而使用sizeof,因为缓冲区已用已知字符串进行初始化,其长度是固定的,所以sizeof是在编译时计算缓冲区长度。

PS. 标准I/O库是带缓冲的。如果标准输出连接到终端设备,则它是行缓冲的,否则它是全缓冲的。当以交互方式运行该程序时,只得到该printf输出的行一次,其原因是标准输出缓冲区由换行符冲洗。但是当将标准输出重定向到一个文件时,却得到printf输出行两次。其原因是,在fork之前调用了printf一次,但当调用fork时,该行数据仍在缓冲区中,然后将父进程数据空间复制到子进程中时,该缓冲区数据也被复制到子进程中,此时父进程和子进程各自有了带该行内容的缓冲区。在exit之前的第二个printf将其数据追加到已有的缓冲区中。

fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。此处”复制“相当于对文件描述符执行了dup函数。父进程和子进程每个相同的打开描述符共享了一个文件表项。因为共享了文件表项(Linux中的file结构体),所以共享了文件偏移量。如果父进程和子进程在写同一文件时没有同步,那么它们的输出会混合。

在fork之后处理文件描述符有以下两种常见情况:

-父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏量已做出了相应更新。

-父进程和子进程各自执行不同的程序段。在这种情况下,在fork之后,父进程和子进程各自关闭它们不需要使用的文件描述符,这样就不会干扰对方使用的文件描述符。(网络服务器进程常用)。

除了打开文件之外,父进程的很多其它属性也由子进程继承,包括:

-实际用户ID、实际组ID、有效用户ID、有效组ID;

-附属组ID;

-进程组ID;

-会话ID;

-控制终端;

-设置用户ID标志和设置组ID标志;

-当前工作目录;

-根目录;

-文件模式创建屏蔽字;

-信号屏蔽和安排;

-对任一打开文件描述符的执行时关闭标志;

-环境;

-连接的共享存储段;

-存储映像;

-资源限制;

父进程和子进程的区别具体如下:

-fork的返回值不同;

-进程ID不同;

-这两个进程的父进程ID不同;

-子进程的tms_utime、tms_stime、tms_cutime和tms_ustime的值设置为0;

-子进程不继承父进程设置的文件锁;

-子进程的未处理闹钟被清除;

-子进程的未处理信号集设置为空集;

 

fork·失败主要有两个原因:

-系统中已经有太多的进程;

-该实际用户ID的进程总数超过了系统限制(CHILD_MAX每个实际用户ID任一时刻可拥有的最大进程数)。

fork有两种用法:

-一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。在网络服务器中经常用到。

-一个进程要执行一个不同的程序。shell常见,fork返回后调用exec函数。

UNIX系统将fork和exec分为两个操作,因此子进程可以在fork之后exec之前更改自己的属性,如I/O重定向、用户ID、信号安排等。

 

函数vfork:

vfork函数的调用序列和返回值与fork相同,但两者语义不同。

vfork函数用于创建一个新进程,而该新进程的目的是exec一个新程序。vfork也会创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会引用该地址空间。不过子进程在调用exec或exit之前会在父进程地址空间运行。如果子进程修改数据(除了用于存放vfork返回值的变量)、进行函数调用或者没有调用exec或exit就返回都可能会带来未知的结果。

vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行,当子进程调用这两个函数中的任意一个时,父进程会恢复运行。

 

函数exit:

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

我们希望终止进程能够通知其父进程它是如何终止的。对于3个终止函数(exit/_exit/_Exit),实现这一点的方法是,将其退出状态(exit status)作为参数传送给函数。在异常终止情况,内核(不是进程本身)产生一个指示其异常终止原因的终止状态(termination status)。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。(在调用_exit函数时,内核将退出状态转换为终止状态。

孤儿进程:对于父进程已经终止的所有进程,它们的父进程都改变为init进程。称由init进程收养。在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则该进程的父进程ID就更改为1.这种处理方法保证每个进程都有一个父进程。

内核为每个终止子进程保存了一定量信息,所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息。这些信息至少包含进程ID、该进程的终止状态以及该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。

僵尸进程:一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息,释放它仍占用的资源)的进程被称为僵尸进程。

init被编写为无论何时只要有一个子进程终止,init就会调用一个wait函数取得其终止状态,防止出现过多僵尸进程。

 

函数wait和waitpid:

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

调用wait或waitpid的进程可能会发生:

-如果其所有子进程都还在运行,则阻塞;

-如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。

-如果它没有任何子进程,则立即出错返回。

#include<sys/wait.h>

pid_t wait(int *statloc);

pid_t waitpid(pid_t pid, int *statloc, int options);

  若成功,返回进程ID,若出错,返回0或-1;

-在一个子进程终止前,wait使其调用者阻塞,而waitpid有一选项,可使调用者不阻塞。

-waitpid并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。

wait会返回调用进程的第一个终止子进程,可能会是僵尸进程。返回该子进程的进程ID。

参数statloc是一个整型指针,如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的单元中。如果不关心终止状态,则可将参数指定为空指针。有四个互斥宏可取得进程终止状态原因:

WIFEXITED(status);若为正常终止子进程返回的状态,则为真;这种情况可执行WEXITSTATUS(status)获取子进程传送给exit或_exit参数的低8位。

WIFSIGNALED(status);若为异常终止子进程返回的状态,则为真;可执行WTERMSIG(status)获取使子进程终止的信号编号。

WIFSTOPPED(status);若为当前暂停子进程返回的状态,则为真;可执行WSTOPSIG(status)获取使子进程暂停的信号编号。

WIFCONTINUED(status);若在作业控制暂停后已经继续的子进程返回了状态,则为真。仅用于waitpid。

waitpid函数中参数pid的作用:

-pid==-1  等待任一子进程。这种情况下与wait等效。

-pid>0  等待进程ID与pid相等的子进程。

-pid==0  等待组ID等于调用进程组ID的任一子进程。

-pid<-1  等待组ID等于pid绝对值的任一子进程。

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

 options参数使我们能进一步控制waitpid的操作。此参数是0或以下常量按位运算的结果:

-WCONTINUED,若实现支持作业控制,那么由pid指定的任一子进程在停止后已经继续,但其状态尚未报告,则返回其控制状态。

-WNOHANG,若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返回值为0.

-WUNTRACED,若某实现支持作业控制,而由pid指定的任一子进程已处于停止状态,并且其状态自停止以来还未报告过,则返回其状态。WIFSTOPPED宏确定返回值是否对应于一个停止的子进程。

waitpid函数提供了wait函数没有的3个功能:

-waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态。

-waitpid提供了一个wait的非阻塞版本。有时希望获取一个子进程的状态,但不想阻塞。

-waitpid通过WUNTRACED和WCONTINUED选项支持作业控制。

 

函数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,等待已退出的进程;

-WNOHANG,如无可用的子进程退出状态,立即返回而非阻塞;

-WNOWAIT,不破坏子进程的退出状态。该子进程退出状态可由后续的wait、waitid或waitpid调用取得;

-WSTOPPED,等待一进程,它已经停止,但其状态尚未报告。

WCONTINUED、WEXITED或WSTOPPED这3个常量之一必须在options中指定。

 

函数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;

rusage参数允许内核返回由终止进程及其所有子进程使用的资源情况。资源统计信息包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等。

 

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

 

函数exec:

当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec函数并不创建新进程,所以前后的进程ID并未改变。exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。

有7中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[]);

int fexecve(int fd, char*const argv[], char*const envp[]);

  成功时不反悔,出错返回-1.

前4个函数以路径作为参数,接着两个以文件名作为参数,最后一个以文件描述符作为参数。

当指定filename作为参数时:

-如果filename中包含/,则就将其视为路径名;

-否则就按PATH环境变量,在它所指的各目录中搜寻可执行文件;

PATH变量包含一张目录表,各目录以:分隔。

如果execlp和execvp使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编辑器产生的机器可执行文件,则就认为该文件是一个shell脚本,于是试着调用shell程序并以该文件作为shell的输入。

fexecve函数避免了寻找正确的可执行文件,而是依赖调用进程来完成这项工作。调用进程可以使用文件描述符验证所需要的文件并且无竞争地执行该文件。

通常一个进程将其环境变量传递给子进程,但有时也有进程为子进程指定特定的环境变量。exec函数中带e表示需要指定环境变量。l表示list,v表示vector。

每个系统对参数表和环境表的总长度有一个限制,ARG_MAX。

在执行exec后,进程ID没有改变。新程序从调用进程继承了下列属性:

-进程ID和父进程ID;

-实际用户ID和实际组ID;

-附属组ID;

-进程组ID;

-会话ID;

控制终端;

-闹钟尚余留的时间;

-当前工作目录;

-根目录;

-文件模式创建屏蔽字;

-文件锁;

-进程信号屏蔽;

-未处理信号;

-资源限制;

-nice值;

-tms_utime、tms_stime、tms_cutime以及tms_cstime;

对打开文件的处理与每个文件描述符标志执行时关闭有关。

POSIX.1规定exec时关闭打开目录流,通常由opendir实现,它通过调用fcntl实现对目录流的描述符设置执行时关闭标志。

exec前后实际用户ID和实际组ID保持不变,而有效ID是否改变取决于所执行程序文件的设置用户ID和设置组ID位是否被设置。如果设置用户ID位被设置则有效ID变成该程序文件的所有者ID,同理设置组ID位被设置则有效组ID编程程序文件的所有组。。

这7个函数中只有execve是内核的系统调用,另外六个都需要通过execve实现功能。

 

更改用户ID和更改组ID:

在UNIX系统中,特权(如能否改变当前日期的表示法)以及访问控制(如能否读、写一个特定文件),是基于用户ID和组ID的。当程序需要增加特权,或需要访问当前并不允许访问的资源时,我们需要更换自己的用户ID或组ID,使得新ID具有合适的特权或访问权限。降低特权或访问权限时也要更换ID。

#include<unistd.h>

int setuid(uid_t uid);

int setgid(gid_t gid);

  成功时返回0,出错时返回-1;

更改用户ID的规则:

-若进程具有超级用户权限,则setuid函数将实际用户ID、有效用户ID以及保存的设置用户ID设置为uid;

-若进程没有超级用户权限,但是uid等于实际用户ID或保存的设置用户ID,则只将有效用户ID设置为uid;

-如果上述两个条件都不满足,则设置errno位EPERM,并返回-1;

 

内核维护的3个用户ID:

-只有超级用户进程可以更改实际用户ID。通常,实际用户ID是在用户登录时,由login程序设置的,而且绝不会改变它。因为login是一个超级用户进程,当它调用setuid时,设置所有3个ID;

-仅当对程序文件设置了设置用户ID位时,exec函数才设置有效用户ID。如果设置用户ID位没有设置,exec函数不会改变有效用户ID,而将维持其现有值。任何时候都可以调用setuid,将有效用户ID设置为实际用户ID或保存的设置用户ID。不能将有效用户ID设置为任一随机值。

-保存的设置用户ID是由exec复制有效用户ID得到的。如果设置了文件的设置用户ID位,则在exec根据文件的用户ID设置了进程的有效用户ID后,其原来的有效用户ID的副本就保存在保存的设置用户ID。

没有可以值得方法去获得保存的设置用户ID的当前值。

 

#include<unistd.h>

int setreuid(uid_t ruid, uid_t euid);

int setregid(gid_t rgid, gid_t egid);

交换实际用户ID和有效用户ID;交换实际组ID和有效组ID;

若其中任一参数的值为-1,则表示相应的ID应当保持不变。

 

函数seteuid和setegid:

#include<unistd.h>

int seteuid(uid_t uid);

int setegid(gid_t gid);

  成功返回0,出错返回-1;

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

 

解释器文件:

UNIX系统支持解释器文件。文本文件,其其实行的形式为:

  #! pathname [optional-argument]

在感叹号和pathname之间的空格是可选的。如#! /bin/sh

pathname通常是绝对路径名,通过exec函数调用解释器文件可调用解释器(pathname对应的文件)并输入参数[optional-argument]

 

函数system:

#include<stdlib.h>

int system(const char*cmdstring);

如果cmdstring是空指针,则仅当命令处理程序可用时,system返回非0值,这一特征可以确定在一个给定的操作系统上是否支持了system函数。

因为system在其实现中调用了fork、exec和waitpid,因此有3种返回值:

-fork失败或者waitpid返回除EINTR之外的出错,则system返回-1,并且设置errno以指示错误类型。

-如果exec失败(表示不能执行shell),则返回值如同shell执行了exit(127)一样。

-否则所有3个函数(fork、exec、waitpid)都执行成功,那么system的返回值是shell的终止状态。

使用system相比fork加exec的优点是:system提供了所需的各种出错处理和信号处理。

 

进程会计:

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

accton命令;

 

用户标识:

#include<unistd.h>

char *getlogin(void);

  成功返回指向登录名字的字符串,出错返回NULL;

如果调用此函数的进程没有连接到用户登录时所用的终端,则会失败。通常称这样的进程为守护进程。

有了登录名,就可以用getpwname在口令文件中获得其登录的shell。

 

进程调度:

调度策略和调度优先级是内核确定的,进程可以通过调整nice值选择以更低优先级允许,只有特权进程允许提高调度权限。

nice值越小,优先级越高。nice值范围在0~(2*NZERO)-1之间。NZERO是默认的nice值。

#include<unistd.h>

int nice(int incr);

  成功返回新的nice值-NZERO;出错返回-1;

incr参数被加到进程的nice值上,如果incr太大,系统将它降成最大合法值。如果incr太小,系统将它提高成最小合法值。由于-1是合法的成功返回值,所以要判断errno的值确定是否成功。如果返回值-1,errno为0,则表示成功返回;如果返回值-1,errno不为0,则失败。

#include<sys/resource.h>

int getpriority(int which, id_t who);

  成功返回-NZERO~NZERO-1之间的nice值;出错返回-1;

which参数可指定三个值之一:PRIO_PROCESS表示进程,PRIO_PGRP表示进程组,PRIO_USER表示用户ID。which参数控制who是如何解释的,who参数选择感兴趣的一个或多个进程。如果who为0,表示调用进程、进程组或用户。当which设置为PRIO_USER并且who为0时,表示获得进程的实际用户ID。如果which参数作用于多个进程,则返回所有进程中优先级最高的。

#include<sys/resource.h>

int setpriority(int which, id_t who, int value);

  成功返回0,出错返回-1;

 

进程时间:

墙上时钟时间、用户CPU时间、系统CPU时间

#include<sys/times.h>

clock_t times(struct tms*buf);

  成功返回流逝的签上时钟时间,失败返回-1;

struct tms{

  clock_t tms_utime;  //用户CPU时间;

  clock_t tms_stime;  //系统CPU时间;

  clock_t tms_cutime;  //用户CPU时间,终止的孩子;

  clock_t tms_cstime;  //系统CPU时间,终止的孩子;

};

所有用此函数返回的clock_t值都要除以_SC_CLK_TCK(时钟一秒滴答数)得到实际秒数。

 

以上是关于UNIX进程控制的主要内容,如果未能解决你的问题,请参考以下文章

Unix高级编程之进程控制

1.5 常用UNIX/Linux命令 -进程控制类命令

UNIX为啥要把PCB分为进程表项proc区和U区

《Unix&Linux大学教程》学习笔记七:进程与作业控制

Supervisor-类unix系统下的进程控制工具

Unix线程概念控制原语属性