Unix进程控制

关于进程的定义,可以参照wiki,这里就不多说了。

进程标识


每个进程都有一个非负整数表示的唯一进程ID(PID)。和文件描述符一样,进程ID是可以复用的。当一个进程终止后,其进程ID就成为了复用的候选者。

大多数Unix使用延迟复用算法,使得赋予新建进程的PID不同于最近终止进程所使用的PID。这防止了将新近进程误认为是使用同一PID的某个先前终止的进程。这一点上同文件描述符是有所不同的,前者总是返回可重用的候选者之中最小的。

PID为0的进程通常是调度进程,常被称为交换进程。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程。PID为1的通常是init进程,在自举过程结束时由内核调用。init进程决不会终止。它是一个普通的用户进程,但它却以超级用户的特权进行。而且它会成为所有孤儿进程的父进程。

下面的这些函数可以返回进程的一些标识符

#include <unistd.h>

pid_t getpid(void);           /*  返回调用进程的PID  */
pid_t getppid(void);          /*  返回调用进程的父进程的PID  */

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,父进程返回子进程PID;若出错,则返回-1  */

由fork创建的进程被称为子进程。fork被调用一次,但返回两次。
之所以将子进程ID返回给父进程,是因为一个进程可以有多个子进程,并且没有一个函数可以获得其所有子进程的PID。而使子进程得到返回值0的理由是:一个进程只会有一个父进程,它总可以调用getppid函数得到其父进程的PID

子进程会拷贝父进程的数据空间以及堆和栈,即子进程是其父进程的一个副本。父子进程共享程序的正文段

由于fork之后经常跟随着exec,所以现在很多实现并不拷贝一个父进程数据段、堆和栈的完全副本。作为替代,使用了写时复制技术

所谓写时复制,即这些区域由父子进程共享,而且内核将它们的访问权限更改为只读。如果父子进程中的一个企图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟内存中的一页。

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

int globvar = 5;
char buf[] = "hello, world\n";

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

    if (write(1, buf, sizeof(buf) - 1) != sizeof(buf) - 1)
        fprintf(stderr, "write error");
    printf("there is a buf\n");

    if ((pid = fork()) < 0)
        fprintf(stderr, "fork error");
    else if (pid == 0) {    /*  子进程  */
        globvar++;
        var++;
    } else
        sleep(2);    /*  父进程休眠2秒,以等待子进程  */

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

运行结果如下:

扫描二维码关注公众号,回复: 2560320 查看本文章
$ ./a.out

hello, world
there is a buf
pid = 95382, glob = 6, var = 13
pid = 95381, glob = 5, var = 12

很明显子进程中变量值的改变并没有影响到父进程中变量的值。但当我们将结果重定向到一个文件中去时,结果又有所不同了。

$ ./a.out > tmp
$ cat tmp

hello, world
there is a buf
pid = 95405, glob = 6, var = 13
there is a buf
pid = 95404, glob = 5, var = 12

可以注意到,there is a buf 输出了两次,而非一次。我们知道,write是不带缓冲的。因为在fork之前调用write,所以其数据写到标准输出一次。

但是标准I/O库是带缓冲的,如果输出与终端设备相关连,则是行缓冲的。所以当以交互式方式运行该程序时,只得到printf输出的行一次,原因就是标准输出缓冲区由换行符冲洗

而将标准输出重定向到一个文件中时,却得到printf输出的行两次,原因就是此时标准输出已不再是行缓冲的了,而是全缓冲,由于子进程拥有父进程的一份拷贝,自然也就有了该缓冲区中的内容,在exit之前的第二个printf将其数据追加到已有的缓冲区中。当每个进程终止时,其缓冲区中的内容被写到相应的文件中去。


关于上面的程序还应注意的一点是,当父进程的标准输出被重定向时,子进程的标准输出同样会被重定向。fork的一个特性是父进程的所有打开文件描述符都被复制到了子进程中,而且父子进程每个相同的打开文件描述符共享一个文件表项。当然除此之外,父进程的许多其他属性也为子进程所继承。

更重要的一点是,父进程与子进程共享同一个文件偏移量。这样,当父子进程同时向一个文件中写入时才不会发生混乱。在本例中,当父进程等待子进程时,子进程写到标准输出;而在子进程终止后,父进程也写到标准输出,并且知道其输出会追加到子进程所写的数据之后。

但如果父进程没有等待子进程结束,则上述描述就是不成立的。它们的输出很可能会混合在一起。

在fork之后处理文件描述符通常有以下这两种情况。

父进程等待子进程结束。当子进程终止后,它曾经读写过的文件描述符的文件偏移量已经作了相应更新。
父子进程各自执行不同的程序段。fork之后各自关闭它们不需使用的文件描述符,防止互相干扰。

因为fork之后经常会立即exec,所以某些操作系统将这两个操作合并成一个操作。但在Unix中,将它们分开是有其重要原因的,因为在很多场合需要单独使用fork,并且这样可以使得子进程在exec之前去更改自己的属性,如I/O重定向、用户ID、信号安排等。

char *argv[2];
argv[0] = "cat";
argv[1] = 0;
if(fork() == 0) {
    /*  在exec之前对子进程进行修改,将input.txt重定向到标准输入  */
    close(0);
    open("input.txt", O_RDONLY);   /*  返回的fd保证是0  */ 
    exec("cat", argv);
}

终止进程


进程有5种正常终止及3种异常终止。

正常终止:

  • 在main函数中执行return语句,这等效于调用exit
  • 调用exit
  • 调用_Exit(ISOC)或_exit(系统调用)。两者对标准I/O流并不进行冲洗。
  • 进程的最后一个线程在其启动例程中执行return语句
  • 进程的最后一个线程调用pthread_exit函数

异常终止:

  • 调用abort
  • 当进程接收到某些信号时
  • 最后一个线程对”取消”请求作出响应

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立即返回并取得该子进程的状态;否则wait使其调用者阻塞,直到一个子进程终止。

参数statloc中存储着进程的终止状态,如果不关心终止状态,可将它置空。

如果想要等待某个特定的子进程终止,则可以使用waitpid函数,对于waitpid中的pid参数的作用解释如下:

参数 作用
pid > 0 等待进程ID与pid相等的子进程
pid == 0 等待组ID等于调用进程组ID的任一子进程
pid == -1 等待任一子进程,此时等价于wait
pid < 1 等待组ID等于pid绝对值的任一子进程

options参数使我们进一步控制waitpid的操作,此参数或是0,或是下表所示常量按位或运算的结果

常量 说明
WCONTINUED 若实现支持作业控制,那么由pid指定的任一子进程在终止后仍在继续,但其状态尚未报告,则返回其状态
WNOHANG 若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返回值为0
WUNTRACED 若实现支持作业控制,而由pid指定的任一子进程已处于终止状态,并且其状态自终止以来还从未报告过,则返回其状态

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;若成功,不返回  */

猜你喜欢

转载自blog.csdn.net/qq_41145192/article/details/81394503