APUE学习:第八章进程控制

进程ID相关函数

fork函数

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

tips:

1.调用一次返回两次:子进程返回0,父进程返回子进程ID

2.采用了写时复制技术:子进程和父进程共享数据段、栈和堆的完全副本,这些区域的访问权限变为只读。如果父子进程任一一个试图更改这些区域,则为修改区域的那块内存制作一个副本

 示例代码:

第二行是父进程(sleep之后才输出),值发生了改变。

一些上例代码的细节:

#include"apue.h"
int globvar=6;
char buf[]="a write to stdout\n";


int main()
{
    int var;
    pid_t pid;
    var=88;
    if((write(STDOUT_FILENO,buf,sizeof(buf)-1))!=sizeof(buf)-1)
    err_sys("write error");
    printf("before fork\n");
    if((pid=fork())<0)
    {
        err_sys("fork error\n");
    }
    else if(pid==0)//子进程
    {
        globvar++;
        var++;
    }
    else{
        sleep(2);
    }
    printf("pid=%d,glob=%d,var=%d\n",getpid(),globvar,var);
    return 0;
}

1.fork之后父进程先执行还是子进程先执行是不确定的,于是本例中父进程休眠了2s

2.为什么write的时候写的字节数为sizeof(buf)-1?

sizeof计算字符串长度的时候会将字符串的结束符号\0计算进去,所以要-1

或者使用strlen计算字符串长度(不用减1)

3.

 注意到如果将标准输出指向文件,则before fork输出了两次,这是为什么呢??

先解释为什么输出到终端只有一个before fork:

标准输出到终端设备(交互式设备)是行缓冲的,所以当第一次调用printf("before fork\n")时,遇到换行符"\n"就把行缓冲区清空了

在调用fork之后,子进程和父进程共享的堆(缓冲通常被存在堆区中)已经没有缓冲了,所以子进程不用输出before fork。

为什么输出到文件有两个before fork??

因为将标准输出重定向至文件以后,标准输出会变成全缓冲模式。

所以在fork函数之前调用printf("before fork");并不会立马输出到文件中去,而是将before fork\n字符串存在缓冲区(缓冲区在堆区中)中。

那么什么时候才将缓冲区清空呢?

等到fork函数调用以后,子进程、父进程共享堆区并且当子进程、父进程结束之后,才将缓冲区中的内容写入文件。所以子进程、父进程都会将before fork写入文件。

上图代码中是子进程先执行,子进程和父进程的缓冲区是共享的,为什么子进程对缓冲区进行冲洗以后,父进程还能输出before fork??

这就是写时复制,当子进程对缓冲区(堆区中的一部分)进行冲洗时,相当于对堆区进行了修改,则内核在子进程进行冲洗前会生成一个缓冲区的副本。

4.fork会将父进程的所有打开文件描述符都复制到子进程中(好像对于每个文件描述符都调用了dup)。

子进程和父进程共享同一文件偏移量

也就是说子进程写完父进程再写,则父进程写的内容会追加在子进程写的内容后面

fork函数失败原因:

1.系统中已经有了太多进程

2.该实际用户ID的进程总数超过了系统限制

vfork函数

与fork函数的不同点:

vfork不采用写时复制技术,在子进程运行时会直接使用父进程的地址空间,不会进行复制操作。

vfork感觉不重要,函数库里面都没有(敲了代码发现ide找不到这个函数)。

函数exit

对于父进程已经终止的所有进程,它们的父进程都改变为init进程,

 内核为每个终止进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息。

僵死进程:已经终止,父进程尚未对其进行善后处理的进程。

一个init进程收养的子进程终止时,init会调用wait函数取得其终止状态(算是前面提到的“善后处理”),防止了系统中塞满僵死进程。

函数wait和waitpid

返回值pit_t为一个整形,为子进程的pid

传入statloc用来存放子进程的终止状态,可以设置为空指针,表示不接受子进程的终止状态

父进程调用wait、waitpid

1.所有子进程都还在运行,则阻塞

2.有终止子进程,则获取子进程的终止状态并立即返回

3.没有任何子进程,出错返回

wait和waitpid 的区别:

1.在所有子进程都还在运行时,父进程调用wait会阻塞,waitpid有一选项可使父进程不阻塞

对于wait,唯一的出错是调用进程没有子进程

对于waitpid指定的进程或者进程组不存在,或者pid指定的进程不是调用进程的子进程,都可能出错。

waitpid的options参数能进一步控制waitpid的操作,此参数可以为0,或者

2.waitpid可以指定等待的进程

status如何使用??

代码示例:

#include "apue.h"
#include <sys/wait.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));
}

pr_exit用于打印进程终止状态的说明。

pr_exit和wait函数的使用示例:

#include"apue.h"
#include<sys/wait.h>
#include"prexit.c"

int main()
{
    pid_t pid;
    int status;
    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);
    return 0;
}

如果一个进程fork一个子进程,但不要它等待子进程终止,也不希望子进程处于僵死状态直到父进程终止,要怎么办?—— fork两次

示例代码:

#include"apue.h"
#include<sys/wait.h>
int main()
{
    pid_t pid;
    if((pid = fork())<0)
    err_sys("fork error");
    else if(pid == 0) //first child == parent from second fork
    {
        printf("第一个子进程的id为:%d\n",getpid());
        if((pid=fork())<0)
        err_sys("fork error");
        else if(pid>0)/*first child*/
        exit(0);
        sleep(2);//等待初始父进程退出
        printf("second child,parent pid = %ld\n",(long)getppid());
        exit(0);

}
if(waitpid(pid,NULL,0)!=pid)
    err_sys("waitpid error");
printf("初始父进程退出.");
exit(0);

}

理解:上例程序涉及到3个进程爷进程、子进程、孙进程

代码思路就是:先用爷进程创建一个子进程,然后在子进程中调用fork函数

子进程调用exit进入僵死状态,爷进程调用waitpid对子进程进行善后工作,然后爷进程终止。

这样孙进程就变成了孤儿进程从而被init进程收养,就不用担心孙子进程的善后工作。

函数waitid

函数原型及其参数解释:

函数wait3和wait4

提供的功能比 wait、waitpid和waitid所提供的功能多一个,附加参数允许内核返回由终止进程及其所有子进程的资源情况

竞争条件

定义:多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序。

子进程需要等待父进程终止可以使用:

while(getppid()!=1)

sleep(1);

竞争示例代码:

#include"apue.h"
static void charatatime(char *);
int main()
{
    pid_t pid;
    if((pid=fork())<0)
        err_sys("fork error");
    else if(pid==0)
    {
        charatatime("output from child\n");
    }
    else
    {
        charatatime("output from parent\n");
    }
    return 0;
}

static void charatatime(char *str)
{
    char *ptr;
    int c;
    setbuf(stdout,NULL);//将标准输出设置为无缓冲
    for(ptr=str;(c=*ptr++)!=0;)//ASCII码为0的表示空字符\0
    {
        putc(c,stdout);
    }
}

输出结果:

 putc函数原型:int putc(int character, FILE *stream);

将字符写入指定的文件流中

内核进行进程调度,父进程和子进程往标准输出输出字符的顺序是不定的。

函数exec

1.进程调用exec函数后进程执行的程序完全替换为新程序,并从main函数开始执行

2.exec不创建新进程,进程ID不变

3.exec用磁盘尚的一个新程序替换了当前的正文段、数据段、堆段、栈段

4.exec前后实际用户ID和实际组ID保持不变,有效ID是否改变取决于程序文件的设置用户ID位,和设置组ID位

exec类函数区分

execlp、execvp还可以运行shell脚本

名字中带p(PATH)的表示传入的是filename

第二个区别与参数表的传递有关 

execl   execv

函数名字中的l表示列表list(一个一个参数传入),需要以一个空指针结尾(可以用(char *) 0表示)

eg:char *arg0,char *arg1,.....,(char *)0

v表示矢量vector(传入一个数组)

最后一个区别于向新程序传递环境表相关

函数名字中的e  execle、execve和fexecve

表示可以传递一个指向环境字符串指针数组的指针

文件描述符继承

exec打开的新程序有没有继承原进程打开的文件描述符呢? ——取决于打开文件时是否有close-on-exec标志

FD_CLOEXEC

在open时设置了FD_CLOEXEC则在执行exec时关闭该描述符.

exec函数示例:

#include"apue.h"
#include<sys/wait.h>
char *env_init[]={"USER=unknown","PATH=/tmp",NULL};
int main()
{
    pid_t pid;
    if((pid = fork())< 0)
        err_sys("fork error");
    else if(pid == 0)
    {
        if(execle("/home/yesheng/echoall.out","echoall","myarg1","MY ARG2",(char *)0,env_init)<0)
            err_sys("execle error");

    }
    if(waitpid(pid,NULL,0)<0)
        err_sys("wait error");
    if((pid = fork())<0)
        err_sys("fork error");
    else if(pid==0)
    {
        if(execlp("./echoall.out","echoall","only 1 arg",(char *)0)<0)
            err_sys("execlp error");
    }
    return 0;
}

echoall.c代码如下:

#include"apue.h"
int main(int argc,char *argv[])
{
    int i;
    char **ptr;
    extern char **environ;
    for(i=0;i<argc;i++)
        printf("argv[%d]:%s\n",i,argv[i]);
    for(ptr = environ;*ptr!=0;ptr++)
    printf("%s",*ptr);
    return 0;
}

输出代码太长了,execlp输出的环境变量太多了!

更改用户ID和更改组ID

还需注意:

1.只有超级用户进程可以更改实际用户ID

eg:登录进程login就是超级用户进程,用户登录时,由login进程设置实际用户ID

2.仅当对程序文件设置了设置用户ID位时,exec函数才设置有效用户ID

3.保存的设置用户ID是由exec复制有效用户ID得到的。

eg:执行program的进程有效用户ID为yesheng,而program设置有效ID为oujun,那么在

exec program时 yesheng将会被保存起来,将有效用户ID设置为oujun

等从exec返回时再将有效用户ID设置为yesheng

函数setreuid、setregid

功能:交换实际用户ID和有效用户ID

解释器文件

文件类型:文本文件

起始行形式:#! pathname[optional-argument]

eg:   #!/bin/bash

...............不重要,用到再看

函数system

 总结:就是在C程序中执行shell命令罢了

代码示例:

#include"apue.h"
#include<sys/wait.h>
#include"prexit.c"

int main()
{
    int status;
    if((status = system("date"))<0)
        err_sys("system() error");
    if((status = system("nosuchcommen"))<0)
        err_sys("system() error");

    pr_exit(status);
    if((system("who;exit 44"))<0)
        err_sys("system() error");
    pr_exit(status);
    return 0;
}

输出结果:

下面是关于system的一种实现:

 

用fork和exec也可以执行shell脚本(上面学到的解释器),但是system进行了所需的各种出错处理以及各种信号处理.

注意:设置用户ID或者设置用户组ID程序决不应该调用system函数 (书上例子看不懂留着吧

进程会计(暂时跳过,对于我的项目次要)

进程调度

提供基于调度优先级的粗粒度的控制。

注意区分nice值和NZERO值:

nice表示进程优先级,nice值越小,优先级越高(你越友好,你的调度优先级就越低)。

NZERO是系统默认的nice值.

注意:NZERO的头文件因系统而异,除了头文件外Linux可以通过非标准的sysconf参数(_SC_NZERO)来访问NZERO 

eg:

#if defined(NZERO)
    nzero=NZERO;
#elif defined(_SC_NZERO)
    nzero = sysconf(_SC_NZERO);
#else
#error NZERO undefined
#endif

 上述代码获取NZERO使程序具有移植性

nice函数

普通进程可以通过调整nice值选择更低优先级运行,只有特权进程允许提高调度权限。(nice值越小,优先级越高)

incr参数将增加到调用进程的nice值上,系统会控制nice值为合法的(普通进程只允许增加nice值,只有特权用户允许减少nice值),nice值范围为-20到19

注意:若出错,则返回-1;若返回-1则不一定出错(因为-1为nice的合法值)

所以在调用nice函数前应先清除errno的值,若nice函数返回-1,则检查errno是否出错

可以通过nice(0)获取该进程的nice值

nice函数的返回值是相对于系统基本优先级(NZERO)的偏移量,为了得到实际的优先级值需要将nice的返回值加上NZERO

getpriority函数

setpriority函数

代码示例:

#include"apue.h"
#include<errno.h>
#include<sys/time.h>

#if defined(MACOS)
#include<sys/syslimits.h>
#elif defined(SOLARIS)
#include<limits.h>
#elif defined(BSD)
#include<sys/param.h>
#endif

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 = %lld\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
#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;/* run for 10 seconds*/
    if((pid = fork()) < 0 )
    err_sys("fork failed");
    else if (pid == 0)
    {
        s="child";
        printf("current nice val in child is %d,adjusting by %d\n",nice(0)+nzero,adj);
        errno = 0;//调用nice前先将errno清0
        if((ret = nice(adj))==-1&&errno!=0)
            err_sys("child set scheduling priority");
        printf("now child nice val is %d\n",ret+nzero);
    } else
    {
        s="parent";
        printf("current nice val in parent is %d\n",nice(0)+nzero);
    }

    for(;;)
    {
        if(++count == 0)
            err_quit("%s counter wrap",s);
        checktime(s);

    }

}

上例代码分别调用父进程和子进程进行计数,可以运行./testnice.out n 来调整子进程的优先级

输出结果:

 第一次子进程、父进程的优先级一样,计数也差不多

第二次子进程的优先级进行了调整(降低了20),发现计数值也差不多,这与系统的进程调度有关,书上就差很多

进程时间

三个时钟概念:

1.墙上时钟时间:实际的物理时间,eg:

假设现在是 2024 年 3 月 27 日,早上 8 点。如果我们记录当前的墙上时钟时间,可能会得到类似于以下的时间戳:

墙上时钟时间:2024-03-27 08:00:00 UTC

2.用户CPU时间:CPU用于执行用户进程代码的时间。包括进程执行自己代码的时间,但不包括系统调用或其他内核操作所耗费的时间。

3.系统CPU时间:CPU用于执行内核操作(例如系统调用、I/O操作)的时间。

这三种时间可以调用times函数获取:

注意:tms结构不包含墙上流逝时间,times函数以墙上流逝时间为返回值.

 

这两个参数包含了此进程用wait函数族已等待到的各子进程的值

所有clock_t值都用_SC_CLK_TCK转化为秒数

eg:

代码示例: 

#include"apue.h"
#include<sys/time.h>
#include"/usr/include/x86_64-linux-gnu/sys/times.h"
#include"prexit.c"
static void pr_time(clock_t,struct tms *,struct tms *);
static void do_cmd(char *);
int main(int argc,char *argv[])
{
    int i;
    setbuf(stdout,NULL);
    for(i=1;i<argc;i++)
        do_cmd(argv[i]);
    exit(0);

}
static void do_cmd(char * cmd)
{
    struct tms tmsstart,tmsend;
    clock_t start,end;
    int status;
    printf("\ncommend:%s\n",cmd);
    if((start=times(&tmsstart))<0)
        err_sys("times error");
    if((status = system(cmd))<0)
        err_sys("system error");
    if((end = times(&tmsend))<0)
        err_sys("times error");
    pr_time(end-start,&tmsstart,&tmsend);
    pr_exit(status);
}

static void pr_time(clock_t real,struct tms *tmsstart,struct tms * tmsend)
{
    static long clktck=0;
    if(clktck == 0)
        if((clktck = sysconf(_SC_CLK_TCK))<0)
            err_sys("sysconf error");
    printf(" real: %7.2f\n",real/(double)clktck);
    printf(" user: %7.2f\n",(tmsend->tms_utime-tmsstart->tms_utime)/(double)clktck);
    printf(" sys: %7.2f\n",(tmsend->tms_stime-tmsstart->tms_stime)/(double)clktck);
    printf(" child user: %7.2f\n",(tmsend->tms_cutime-tmsstart->tms_cutime)/(double)clktck);
    printf(" child sys: %7.2f\n",(tmsend->tms_cstime-tmsstart->tms_cstime)/(double)clktck);

}

输出结果:

习题(先放着,我赶时间做项目) 

  • 11
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值