进程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);
}
输出结果: