APUE学习笔记(八)进程控制

8.1 进程标识

每个进程都有一个非负整型表示的唯一进程ID。进程ID是可重用的。

ID为0的进程通常是调度进程,常常被称为交换进程(swapper)。 该进程是内核的一部分,它并不执行任何磁盘上的程序。
ID为1通常是init进程,在自举过程结束时由内核调用。

#include 
#include 
#include 

int main(int argc, char* argv[]) {
    printf("%d\n", getpid());
    printf("%d\n", getppid());
    printf("%d\n", getuid());
    printf("%d\n", geteuid());
    printf("%d\n", getgid());
    printf("%d\n", getegid());
    exit(0);
}

8.2 fork

一个现有的进程可以调用fork函数创建一个新进程。子进程获得父进程数据空间、堆和栈的副本。 父进程和子进程共享正文段。

作为优化,某些实现中不完全创建副本,而使用写时复制技术。父进程和子进程共享这些区域,但是如果任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本。

例子

#include "apue.h"

int globalvar = 6;
char buf[] = "hello, huahua\n";

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

    var = 88;
    if (write(STDOUT_FILENO, buf, sizeof(buf) -1) != sizeof(buf) - 1) //sizeof包含了最后的null
        err_sys("write error");

    printf("before fork\n");
    //写入缓冲区,如果是在shell运行是行缓冲,被换行符刷新;
    // 如果是写入到文件是全缓冲,不会刷新,缓冲内容会带到子进程

    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0) {  //子进程
        globalvar++;  //数据空间独立,只有子进程的变量会被修改
        var++;
    } else {  //父进程
        sleep(2);
    }
    printf("pid: %d, global var: %d, var: %d\n", pid, globalvar, var);
    exit(0);
}

父进程和子进程每个相同的打开描述符共享一个文件表项。父进程和子进程共享同一个文件偏移量。

在fork之后处理文件描述符的方式:

  • 在fork之后处理文件描述符
  • 父进程和子进程各自关闭它们不需使用的文件描述符

使fork失败的两个主要原因:

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

fork有以下两种用法:

  • 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。
  • 一个进程要执行一个不同的程序。

8.3 vfork

vfork与fork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec或exit。

在子进程调用exec或exit之前,它在父进程的空间中运行。

vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。

#include "apue.h"

int globalvar = 6;

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

    var = 88;

    printf("before fork\n");

    if ((pid = vfork()) < 0)
        err_sys("fork error");
    else if (pid == 0) {  //子进程
        globalvar++;  //会修改父进程的数据
        var++;
        _exit(0); //退出时不会刷新缓冲区,如果同时关闭了fd,则不会输出任何内容
    }
    printf("pid: %d, global var: %d, var: %d\n", pid, globalvar, var);
    exit(0);
}

8.4 exit函数

进程终止的情况

(1) 从main返回;
(2) 调用exit, 执行各种终止处理程序
(3) 调用_exit或_Exit,不执行终止处理程序或信号处理程序
(4) 最后一个线程从其启动例程返回。线程的返回值不是进程的终止值。
(5) 从最后一个线程调用pthread_exit
(6) 调用abort,它产生SIGABRT信号。
(7) 接到一个信号,信号可由进程自身(如调用abort函数)、其他进程或内核产生
(8) 最后一个线程对取消请求做出响应

不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。该终止进程的父进程都能用wait或waitpid函数取得其终止状态。

孤儿进程

对于父进程已经终止的所有进程,它们的父进程都改变为 init 进程。只要有一个子进程终止,init 就会调用一个wait函数取得其终止状态。所以init的子进程不会成为僵尸进程。

僵尸进程

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

8.5 函数wait和waitpid

调用wait或waitpid的进程时:

  • 如果其所有子进程都还在运行则阻塞。
  • 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
  • 如果它没有任何子进程,则立即出错返回。
#include "apue.h"
#include "sys/wait.h"

void pr_exit(int status) {
    if (WIFEXITED(status))
        printf("normal exit, 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 exits" : "");
#else
               "");
#endif
    else if (WIFSTOPPED(status))
        printf("child stopped, signal number = %d\n", WSTOPSIG(status));

}

int main(void) {
    int status;
    pid_t pid;

    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0)
        exit(7);  //正常退出

    if (wait(&status) != pid)
        err_sys("wait error");
    pr_exit(status);

    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0)
        abort();  //异常退出

    if (wait(&status) != pid)
        err_sys("wait error");
    pr_exit(status);

    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0)
        status /= 0;  //异常退出

    if (wait(&status) != pid)
        err_sys("wait error");
    pr_exit(status);

    exit(0);
}

waitpid通过指定pid设置不同选项。

pid > 0, 等待指定pid
pid == -1 等待任一子进程
pid == 0 等待组ID等于调用进程组ID的任一子进程
pid < -1 等待组ID等于pid绝对值的任一子进程

8.6 竞争条件

fork之后执行的操作如果对子进程和父进程的执行顺序有要求,则会产生竞争条件。为了处理竞争条件,需要子进程和父进程之间通过信号传递。

例子1,进程竞争展示,输出的字符顺序可能会混乱。

#include "apue.h"

static void charatatime(char *);

int main(void)
{
    pid_t	pid;

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

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

    setbuf(stdout, NULL);			/* set unbuffered */
    for (ptr = str; (c = *ptr++) != 0; )
        putc(c, stdout);
}

添加同步控制

#include "apue.h"

static void charatatime(char *);

int main(void)
{
    pid_t	pid;

    TELL_WAIT();

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {
        WAIT_PARENT();		/* parent goes first */
        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);			/* set unbuffered */
    for (ptr = str; (c = *ptr++) != 0; )
        putc(c, stdout);
}

这里用到的信号处理函数包含在time_wait.c中,在CMakeList中修改

    #添加到可执行文件中
    add_executable(${name} ${file} ../include/error.c
            ../lib/tellwait.c)

8.7 exec函数

当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。

exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。

#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[]);
int fexecve(int fd, char *const argv[], char *const envp[]);

有7种不同的exec函数,这7个函数中只有execve是内核的系统调用。不同之处在于:

  • 可执行文件通过路径名/文件名/fd查找
  • 参数列表传入方式的不同
  • 传入环境表的方式不同

他们相互之间存在调用关系

execlp->execvp->execv->execve
execl->execv->execve
execle->execve
fexecve->execve

例子

#include "apue.h"
#include 

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

int main(void) {
    pid_t pid;

    if ((pid = vfork()) < 0)
        err_sys("fork error");
    else if (pid == 0) {  //子进程
        if (execle("./echoall", "echoall", "hello", "huahua", (char*)0, env_init) < 0)
            err_sys("execle error");
    }

    if (waitpid(pid, NULL, 0) < 0)
        err_sys("wait error");
    
    if ((pid = vfork()) < 0)
        err_sys("fork error");
    else if (pid == 0) {
        if (execlp("echoall", "achoall", "hello", "huahua", (char *)0) < 0)
            err_sys("execlp error");
    }

    exit(0);
}

8.8 解释器文件

就是解释器脚本文件,初始行的格式通常是

#! pathname [ optional-argument ]

pathname指定解释器路径。

内核使调用exec函数的进程实际执行的并不是该解释器文件,而是在该解释器文件第一行中pathname所指定的文件。

解释器文件的好处:

  • 隐藏脚本文件的细节
  • 提高执行效率。执行过程比较复杂。
  • 允许使用其他shell来编写shell脚本。

8.9 system函数

system函数执行传入的命令,在其实现中调用了fork、exec和waitpid。使用system的优点是system进行了所需的各种出错处理以及各种信号处理。

#include 
int system(const char *cmdstring);

例子

#include 
#include 
#include 
#include "apue.h"

int system(const char* cmd) {
    pid_t pid;
    int status;

    if (cmd == NULL)
        return(1);

    if ((pid = fork()) < 0)
        status = -1;
    else if (pid == 0) { //子进程执行传入命令
        execl("/bin/sh", "sh", "-c", cmd, (char*)0);
        _exit(127);
    } else {
        while (waitpid(pid, &status, 0) < 0) { //父进程等待子进程退出
            if (errno != EINTR) {
                status = -1;
                break;
            }
        }
    }
    return status;
}

void pr_exit(int status) {
    if (WIFEXITED(status))
        printf("normal exit, 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 exits" : "");
#else
        "");
#endif
    else if (WIFSTOPPED(status))
        printf("child stopped, signal number = %d\n", WSTOPSIG(status));

}

int main(void) {
    int status;

    if ((status = system("date")) < 0)
        err_sys("system error");

    pr_exit(status);

    if ((status = system("nosuchcommand")) < 0)
        err_sys("system error");

    pr_exit(status);

    if ((status = system("who; exit 44")) < 0)
        err_sys("system error");

    pr_exit(status);

    exit(0);
}

注意,设置用户ID或设置组ID程序决不应调用system函数,有安全问题。如果一个进程正以特殊的权限(设置用户ID或设置组ID) 运行,它又想生成另一个进程执行另一个程序, 则它应当直接使用fork和exec, 而且在fork之后、 exec之前要更改回普通权限。

8.10 用户标识

获取用户登录名。

#include 
char *getlogin(void);

例子

#include "unistd.h"
#include "apue.h"

int main(void) {
    char* user;

    if ((user = getlogin()) != NULL)
        printf("%s", user);

    exit(0);
}

8.11 进程调度

进程的调度策略和调度优先级是由内核确定的。 可以通过nice获得和调整进程的优先级。

进程可以降低优先级,只有特权进程可以提高优先级。

例子

#include "unistd.h"
#include "apue.h"
#include 

int main(void) {
    int pri;
    pri = nice(0);
    printf("nice: %d, errno: %d\n", pri, errno);  //0, 0

    pri = nice(100);
    printf("nice: %d, errno: %d\n", pri, errno); //19, 0

    pri = nice(-100);
    printf("nice: %d, errno: %d\n", pri, errno); //-1, 1

    exit(0);
}

其他函数

#include 
int getpriority(int which, id_t who);
int setpriority(int which, id_t who, int value);

8.12 进程时间

获取进程的系统CPU时间,用户CPU时间和时钟时间。

#include 
clock_t times(struct tms *buf));

例子

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 */
        err_sys("times error");

    if ((status = system(cmd)) < 0)			/* execute command */
        err_sys("system() error");

    if ((end = times(&tmsend)) == -1)		/* ending values */
        err_sys("times error");

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

你可能感兴趣的:(学习记录)