UNIX进程控制

网友投稿 652 2022-09-24

UNIX进程控制

UNIX进程控制

1. 交换进程与init进程

进程ID0是调度进程,常常被称为交换进程(swapper)。该进程并不执行任何磁盘上的程序。它是内核的一部分,因此也被称为系统进程。

进程ID1通常是init进程,在自举过程结束时由内核调用(swapper进程创建一个内核线程,然后exec来执行init)。该进程的程序文件/sbin/init。此进程负责在内核自举后启动一个UNIX系统。init通常读与系统有关的初始化文件(/etc/rc*),并将系统引导到一个状态(例如多用户)。虽然init是一个普通的用户进程(swapper进程是内核的系统进程而非用户进程),但init进程决不会终止。但是它以超级用户特权运行。

在某些UNIX的虚存实现中,进程PID=2是页精灵进程(page daemon)。此进程负责支持虚存系统的请页操作。与交换进程一样,页精灵进程也是内核进程。

2. 下列函数返回相关标识符:

#include

#include

pid_t getpid(void); 返回:调用进程的进程I D

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

#include

#include

pid_t fork(void); //返回:子进程中为0,父进程中为子进程PID,出错为-1

由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程PID。将子进程PID返回给父进程的理由是:因为一个进程的子进程可以多于一个,所以没有一个函数使一个进程可以获得其所有子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getppid以获得其父进程的进程PID(PID=0的进程总是由交换进程使用,所以一个子进程的进程PID不可能为0)。

子进程获得父进程数据空间、堆和栈的复制品。注意,这是子进程所拥有的拷贝。父、子进程并不共享些存储空间部分。如果正文段是只读的,则父、子进程共享正文段。现代很多实现并不做一个父进程数据段和堆的完全拷贝。因为fork之后常常跟着exec,所以作为代替使用了写时复制(copy on write, COW)技术。这些区域由父、子进程共享,而内核将它们设为只读。如果有进程试图修改这些区域,则内核为有关部分,典型的是虚存系统中的“页”,做一个拷贝。

Linux 2.4.22以上的版本提供了一个新进程的系统调用clone,它是一个fork的泛型允许调用者控制哪些部分由父子进程共享。

一般来说,fork之后父进程与子进程哪个先执行时不确定的,取决于内核使用的调度算法。如果要求父子进程之间的同步,则需要某种形式IPC机制.

fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。但是父子进程每个相同的文件描述符共享一个文件表项(如图所示)。这种机制使得父子进程使用同一个文件偏移量。例如假如子进程在fork之后先执行并写入某个父进程打开的文件描述符中,那么之后父进程写入该描述符的数据就会在子进程写入数据之后(在父进程wait等待子进程结束的情形)。如果父子进程并行执行,那么应该在fork之后关闭它们不使用的描述符避免造成混乱(网络服务进程中经常使用)。

其他在fork之后继承的内容:

• UID、GID、EUID、EGID。

• 添加组ID。

• 进程组ID。

• 对话期ID。

• 控制终端。

• SUID和SGID。

• 当前工作目录。

• 根目录。

• 文件方式创建屏蔽字。

• 信号屏蔽和排列。

• 对任一打开文件描述符的在执行时关闭close-on-exec标志。这个标志符的具体作用在于当开辟其他进程调用exec()族函数时,在调用exec函数之前为exec族函数释放对应的文件描述符。

• 环境。

• 连接的共享存储段。

• 资源限制。

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

• fork的返回值。

• 进程ID。

• 不同的父进程ID。

• 子进程的tmsutime,tmsstime , tmscutime以及tmsustime设置为0。

• 父进程设置的锁,子进程不继承。

• 子进程的未决告警(alarm)被清除。信号的”未决“是一种状态,指的是从信号的产生到信号被处理前的这一段时间。

• 子进程的未决信号集设置为空集。

fork常见的两种用法:

(1) 一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待委托者的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。

(2) 一个进程要执行一个不同的程序。这对s h e l l是常见的情况。在这种情况下,子进程在从fork返回后立即调用exec。

4.  vfork

vfork函数的调用序列和返回值与fork相同,用于创建一个新进程,而该新进程的目的是exec一个新程序。它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit)。是也就不会存访该地址空间。不过在子进程调用exec或exit之前,它在父进程的空间中运行。所以子程序在exec之间改变上面程序中的变量就会实际影响到父进程的变量。

vfork和fork之间的另一个区别是: vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。)

#include "apue.h"  int glob = 6;       /* external variable in initialized data */  int main(void) {     int     var;        /* automatic variable on the stack */     pid_t   pid;     var = 88;      printf("before vfork\n");   /* we don't flush stdio */      if ((pid = vfork()) < 0) {         err_sys("vfork error");     } else if (pid == 0) {      /* child */         glob++;                 /* modify parent's variables */         var++;         _exit(0);            }/* child terminates */      /*      * Parent continues here.      */     printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);     exit(0); }

执行程序

$ a.out

before vfork

pid = 607, glob = 7, var = 89

由此可见子进程的执行改变了父进程空间里的变量。如果将_exit改成exit,

$a.out

before vfork

从中可见,父进程printf的输出消失了。其原因是子进程调用了exit,它刷新开关闭了所有标准I/O流,这包括标准输出。虽然这是由子进程执行的,但却是在父进程的地址空间中进行的,所以所有受到影响的标准I/O FILE对象都是在父进程中的。当父进程调用printf时,标准输出已被关闭了,于是printf返回-1。

5. 进程终止

进程终止有三种正常终止法和两种异常终止法:

(1). 正常终止:

(a). 在main函数内执行return语句。这等效于调用exit。

(b). 调用exit函数。此函数由ISO C定义,其操作包括调用各终止处理程序(终止处理程序在调用atexit函数时登录),然后关闭所有标准I/O流等。因为ISO C并不处理文件描述符、多进程(父、子进程)以及作业控制,所以这一定义对UNIX系统而言是不完整的。

(c). 调用_exit系统调用函数。此函数由exit调用,它处理UNIX特定的细节。并不flush标准I/O流。_exit系统调用由exit来调用。_Exit为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法。

(d). 进程的最后一个线程在其启动例程中执行返回语句,但是线程的返回值不会用作进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态0返回。

(e). 进程的最后一个线程调用pthread_exit。进程的终止状态依然与线程的pthread_exit无关,进程终止状态总是0。

(2) 异常终止:

(a) 调用abort。它产生SIGABRT信号,所以是下一种异常终止的一种特例。

(b) 当进程接收到某个信号时。如进程本身(例如调用abort函数)、其他进程和内核都能产生传送到某一进程的信号。例如,进程越出其地址空间访问存储单元,或者除以0,内核就会为该进程产生相应的信号。

无论进程如何终止,最后都会执行内核中同一段代码,这段代码为相应进程关闭打开的文件描述符,释放它所使用的寄存器。并向它的父进程发送一个SIGCHLD,系统默认的处理动作是忽略该信号。

注意,这里使用了“退出状态”(它是传向exit或_exit的参数,或main的返回值。它们将作为参数传递给三个终止函数exit, _exit, Exit)和“终止状态”两个术语,以表示有所区别。在最后调用_exit时内核将其退出状态转换成终止状态。如果子进程正常终止,则父进程可以通过wait和waitpid获得子进程的退出状态。

如果父进程在子进程之前终止,由init进程领养这些孤儿进程成为它们新的父进程。具体过程是当一个进程终止时,内核会逐一检查所有活动的进程,一判断是否是正要终止进程的子进程。如果是,则将它们的ppid改为1。这样就确保每一个进程都有一个父进程。

如果子进程在父进程之前终止,内核为每个终止子进程保存了一定量的信息。(例如Linux的队列中task_struct结构存在只是状态为Zombie)。父进程调用wait或waitpid就可以得到这些信息。包括PID,终止状态,使用的CPU时间等等。然后内核会释放终止进程的所有最后的空间。

如果一个由init进程领养的子进程终止时,它不可能变为zombie进程。因为init设计在任何时候有一个子进程终止,它就会及时调用一个wait函数获得其终止状态,这样就避免了大量zombie进程的产生。

wait和waitpid函数:

#include

#include

pid_t wait(int *statloc);

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

//两个函数返回:若成功则为进程ID,若出错则为-1

在一个子进程终止前, wait使其调用者阻塞,而waitpid有一选择项,可使调用者不阻塞。waitpid并不等待第一个终止的子进程—它有若干个选择项,可以控制它所等待的进程。

如果一个子进程已经终止,是一个僵死进程,则wait立即返回并取得该子进程的状态,否则wait使其调用者阻塞直到一个子进程终止。如调用者阻塞而且它有多个子进程,则在其一个子进程终止时,wait就立即返回。因为wait返回终止子进程的PID,所以它总能了解是哪一个子进程终止了。

int *statloc如果不是一个空指针,则它返回终止进程的终止状态。如果不关心终止状态可以将其设为空指针。关于终止状态可以通过sys/wait.h下定义的一些宏来辅助判断。

waitpid的pid参数可以设置如下:-1等待任一子进程(同wait)。大于0表示等待与pid相等的子进程。0表示子进程组ID等于父进程组ID的任一子进程。小于-1表示子进程组ID等于pid绝对值的任一子进程。

waitpid的options可以为0或者下列或运算的结果,waitpid支持作业控制而wait不支持。

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

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

WCONTINUED若实现作业控制,那么由pid指定的任一子进程在暂停后已经积蓄,但其状态未报告。则返回其状态。

另外的扩展:waitid(Linux不支持), wait3和wait4,更灵活。wait3和wait4还返回了所有终止进程与其子进程的作业汇总。

6. race condition

多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序,这就发生了竞争条件。如果fork之后某种逻辑显式或者隐式依赖于fork之后父进程和子进程的执行顺序(这个顺序不可预知,由内核调度决定),也是race condition经常发生的情形。         如果一个进程希望等待一个子进程终止,则它必须调用wait函数。如果一个进程要等待其父进程终止,则可使用下列形式的循环:     while(getppid() !=1)   sleep(1);     这种形式的循环(称为定期询问(polling))的问题是它浪费了C P U时间,因为调用者每隔1秒都被唤醒,然后进行条件测试。     而通常的的实现是用信号和IPC机制。在子进程中实现TELL_PARENT(),WAIT_PARENT()函数/宏,父进程里实现WAIT_CLILD和TELL_CHILD宏。   7. exec     当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。     exec, fork,wait和exit都是基本的进程控制原语,后面的popen、system之类的函数是基于这些基本的调用来构造的。 #include 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,若成功则不返回*/     后两个如果包含/则被认为是一个路径名,否则则是文件名在PATH环境变量中查找相应的可执行文件。这个可执行文件也可以是一个解释器文件(#!)。     这几个函数中l表示list,v表示向量。list的execl,execle,execlp表示参数的是可变参数列表形式,最后要跟一个空指针表示结束(char *)0     以e结尾的两个函数(execle和execve)可以传递一个指向环境字符串指针数组的指针。其他四个函数则使用调用进程中的environ变量为新程序复制现存的环境。(如果系统支持putenv和setenv也可以在后面生成的子进程中修改,但不能影响父进程的环境)     使用/显示当前进程的所有环境变量: for (char **ptr = environ; *ptr != 0; ptr++) printf("%s\n", *ptr);     在执行exec后,进程ID没有改变。除此之外,执行新程序的进程还保持了原进程的下列特征: • 进程ID和父进程ID。 • 实际用户ID和实际组ID。 • 添加组ID。 • 进程组ID。 • 对话期ID。 • 控制终端。 • 闹钟尚余留的时间。 • 当前工作目录。 • 根目录。 • 文件方式创建屏蔽字。 • 文件锁。 • 进程信号屏蔽。 • 未决信号。 • 资源限制。 • tms_utime, tms_stime, tms_cutime以及tmsustime值。     对打开文件的处理与每个描述符的exec关闭标志值(FDCLOEXEC)有关。若此标志设置,则在执行exec时关闭该描述符,否则该描述符仍打开。除非特地用f c n t l设置了该标志,否则系统的默认操作是在exec后仍保持这种描述符打开。   8. setuid(设置真实的UID)getuid,setreuid,seteuid,setfsuid #include int setuid(uid_t uid) //执行成功则返回0,失败则返回-1,错误代码存于errno。     setuid()用来重新设置执行目前进程的UID。当有效的UID必须为0(root)时。在Linux下,当root 使用setuid()来变换成参数中的UID时(EUID和UID同时变为参数中的uid_t uid),也就是说,该进程往后将不再具有可setuid()的权利,如果只是向暂时抛弃root权限,稍后想重新取回权限,则必须使用seteuid()。如果是非root用户使用setuid这个函数,它只能用来     一般在编写具setuid root的程序时,为减少此类程序带来的系统安全风险,在使用完root权限后建议马上执行setuid(getuid());来抛弃root权限。此外,进程uid和euid不一致时Linux系统将不会产生core dump   9. system函数:执行一个字符串命令。 #include int system(const char* cmdstring); 在实现中调用了fork,exec和waitpid。   10. process accounting     当取了这种选择项后,每当进程结束时内核就写一个会计记录。典型的会计记录是3 2字节长的二进制数据,包括命令名、所使用的CPU时间总量、用户ID和组ID、起动时间等。     会计记录所需的各个数据(各C P U时间、传输的字符数等)都由内核保存在进程表中,并在一个新进程被创建时置初值(例如fork之后在子进程中,内核为子进程初始化一个记录,而不是在新程序exec时。所以会计记录对应的是进程而不是程序。)。进程终止时写一个会计记录。这就意味着在会计文件中记录的顺序对应于进程终止的顺序,而不是它们起动的顺序。为了确定起动顺序,需要读全部会计文件,并按起动日历时间进行排序。 #include #include   #define ACCFILE "/var/adm/pacct" ...... struct acct acdata; ...... if ( (fp = fopen(ACCTFILE, "r")) == NULL) err_sys("can't open file", ACCTFILE);   while (fread(&acdata, sizeof(acdata), 1, fp) == 1) { printf(......acdata.ac_comm...ac.etime) ....... }   10. 获得而用户登陆名,而非用户标志。 #include char *getlogin(void); 得到了登录名,就可用getpwnam在口令文件中查找相应记录以确定其登录shell等。   11. 进程时间 #include clock_t times(struct tms *buf); 获得一个起始时间和一个终止时间,然后经过static long clktck=0; clktck=sysconf(_SC_CLK_TCK); start_times->tms_utime/(double)clktck。 struct tms { clock_t tms_utime; /* CPU time */ clock_t tms_stime; /* system CPU time */ clock_t tms_cutime; /* user CPU time, terminated children */ clock_t tms_cstime; /* system CPU time, terminated children */ }

Textbook:

APUE

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:putty保存修改过的配置
下一篇:珠宝营销史上最著名的营销案例,黑珍珠的故事!(珠宝市场营销典型案例)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~