进程控制:
进程标识:
每个进程都有一个非负整形表示的唯一进程ID。因为进程ID标识符总是唯一的,常将其用作其他标识符的
一部分以保证其唯一性。例如,应用程序有时就把进程ID作为名字的一部分来创建一个唯一的文件名。
虽然是唯一的,但是进程ID是可复用的。当一个进程终止后,其进程ID就称为复用的候选者。大多数的UNIX系统
实现延迟复用算法,使得赋予新建进程的ID不同于最近终止进程所使用的ID。这防止了将新进程误认为将新进程
误认为是使用同一ID的某个已终止的先前进程。
系统中有一些专用进程,,但具体细节随实现而不同。ID为0的进程通常是调度进程,常常被称为交换进程。该进程
是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程。进程ID为1通常是init进程,在自举过程
结束时由内核调用。该进程的程序文件在UNIX的早期版本中时/etc/init,在较新版本中的是/sbin/init。此进程
负责在自举内核后启动一个UNIX系统。init通常读取与系统有关的初始化文件(/etc/rc*文件或/etc/inittab文件,
以及在/etc/init.d中的文件),并将系统引导到一个状态(如多用户)。init进程决不会终止。它是一个普通的用户
进程(与交换进程不同,它不是内核中的系统进程),但是它以超级用户特权运行。
除了进程ID,每个进程还有一些其他标识符。下列函数返回这些标识符:
#include <unistd.h>
pid_t getpid(void); 返回值:调用进程的进程ID
pid_t getppid(void); 返回值:调用进程的父进程ID
uid_t getuid(void); 返回值:调用进程的实际用户ID
uid_t getgid(void); 返回值:调用进程的有效用户ID
gid_t getegid(void); 返回值:调用进程的有效组ID
函数fork:
一个现有的进程可以调用fork函数创建一个新进程:
#include <unistd.h>
pid_t fork(void);
返回值:子进程返回0,父进程返回子进程ID,若出错,返回-1
由fork创建的新进程被称为子进程。fork函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,
而父进程的返回值则是新建子进程的进程ID。将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,
并且没有一个函数使一个进程可以获得其所有子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只会有一个父
进程,所以子进程总是可以调用getppid以获得其父进程的进程ID(进程ID是0总是由内核交换进程使用,所以一个子进程
的进程ID不可能是0)。
例子:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int globvar=6;
char buf[]="a write to stdout
";
int main(void)
{
int var;
pid_t pid;
var=88;
if(write(STDOUT_FILENO,buf,sizeof(buf)-1)!=sizeof(buf)-1)
printf("write error
");
printf("before fork
");
if((pid=fork())<0)
{
printf("fork error
");
}
else if(pid==0)
{
globvar++;
var++;
}
else
{
sleep(2);
}
printf("pid =%ld, glob= %d, var= %d
",(long)getpid(),globvar,var);
exit(0);
}
执行:
./fork
a write to stdout
before fork
pid=430,glob=7,var=89 //子进程的变量值改变了
pid=429,glob=5,var=88 //父进程的变量值没有改变
./fork > temp.out
cat temp.out
a write to stdout
before fork
pid=430,glob=7,var=89
before fork
pid=429,glob=5,var=88
一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。
如果要求父进程和子进程之间相互同步,则要求某种形式的进程间通信。
在上述程序中,父进程使自己休眠2s,以此使子进程先执行。但并不保证2s已经足够。
当写标准输出时,我们将buf长度减去1作为输出字节数,这是为了避免将终止null字节写出。strlen计算不包含
终止null字节的字符串长度,而sizeof则计算包括终止null字节的缓冲区长度。两者之间的另一个差别是,使用
strlen需进行一次函数调用,而对于sizeof而言,因为缓冲区已用已知字符串进行初始化,其长度是固定的,所以
sizeof是在编译时计算缓冲区长度。
函数vfork:
vfork创建一个新进程。
和fork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程回立即调用exec
(或exit),于是也就不会引用该地址空间。不过在子进程调用exec或exit之前,它在父进程的空间中运行。
与fork的区别是:
vfork保证了子进程先运行,在它调用exec或exit之后父进程才能被调度运行,当子进程调用这两个函数中的任意一个时,
父进程会恢复运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致斯锁)。
其中用vfork代替了fork,删除了对于标准输出的write调用。另外,我们也不再需要让父进程调用sleep,因为我们可以
保证,在子进程调用exec或者exit之前,内核会使父进程处于休眠状态。
例子:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int globvar=6;
int main()
{
int var;
pid_t pid;
var=88;
printf("before vfork
");
if((pid=vfork())<0)
{
printf("vfork error
");
}
else if(pid==0)
{
globvar++;
var++;
_exit(0);
}
printf("pid =%ld ,glob=%d,var =%d
",(long)getpid(),globvar,var);
exit(0);
}
执行:
./vfork
before vfork
pid =29039 ,glob=7,var=89
子进程对变量做增1的操作,结果改变了父进程的变量值。因为子进程在父进程的地址空间中运行,所以这并不令人
惊讶。但是其作用的确与fork不同。
此程序调用的是_exit而不是exit。_exit并不执行标准I/O缓冲区的冲洗操作。如果调用的是exit而不是_exit
则该程序的输出是不确定的。它依赖于标准I/O库的实现,我们可能会看到输出没有发生变化,或者发现没有出现
父进程的printf输出。
如果子进程调用的是exit,实现冲洗标准I/O流。如果这是函数库采取的唯一动作,那么我们会见到这样的操作的输出
与子进程调用_exit所产生的输出完全不同,没有任何区别。如果该实现也关闭标准的I/O流,那么表示标准输出FILE
对象的相关存储区将被清0.因为子进程借用了父进程的地址空间。所哟父进程恢复运行并调用printf时,也就不会产生
任何输出,printf返回-1
函数wait和waitpid:
当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件,所哟这种信号也是
内核向父进程发的异步通知。
现在需要知道的是调用wait或waitpid的进程可能会发生什么:
1、如果其所有子进程都还在运行,则阻塞
2、如果一个子进程已终止,正等待父进程获取其终止状态,则取的该子进程的终止状态立即返回
3、如果它没有任何子进程,则立即出错返回
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid,int *statloc,int options);
返回值:成功,返回进程ID,出错,返回0或-1
这两个函数的区别如下:
1、在一个子进程终止前,wait使其调用这阻塞,而waitpid有一选项,可使调用者不阻塞
2、waitpid并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。
如果一个进程fork一个子进程,但不要它等待子进程终止,也不希望子进程处于僵死状态直到父进程终止,实现这一
要求的诀窍是调用fork两次:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
if((pid=fork())<0)
printf("fork error
");
else if(pid==0)
{
if((pid=fork())<0)
printf("fork error
");
else if(pid>0)
exit(0);
sleep(2);
printf("second child,parent pid=%ld
",(long)getppid());
exit(0);
}
if(waitpid(pid,NULL,0)!=pid)
printf("waitpid error
");
exit(0);
}
函数waitid:
此函数类似于waitpid,但是提供了很多的灵活性:
#include <sys/wait.h>
int waitid(idtype_t idtype,id_t id,siginfo_t *infop,int options);
返回值:成功,返回0,出错,返回-1
idtype:
常量 说明
P_PID 等待一特定进程:id包含要等待子进程的进程ID
P_PGID 等待一特定进程组中的任一子进程:id包含要等待子进程的进程组ID
P_ALL 等待任一子进程:忽略id
options参数:各标志的按位或运算。
常量 说明
WCONTINUED 等待一进程,它以前曾被停止,此后又已继续,但其状态尚未报告
WEXITED 等待已退出的进程
WNOHANG 如无可用的子进程退出状态,立即返回而不是阻塞
WNOWAIT 不破坏子进程退出状态。该子进程退出状态可由后续的wait、waitid或waitpid获得
WSTOPPED 等待一进程,它已经停止,但其状态尚未报告
infop参数是指向siginfo结构的指针。该结构包含了造成子进程状态该变有关信号的详细信息。
函数wait3和wait4:
#include <sys/typers.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
pid_t wait3(int *statloc,int options,struct rusage *rusage);
pid_t wait4(pid_t pid,int *statloc,int options,struct rusage *rusage);
程序输出两个字符串:一个由子进程输出,一个由父进程输出。因为输出依赖于内核使这两个进程运行的顺序及每个进程
运行的时间长度,所以该程序包含了一个竞争条件。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
static void charatatime(char *);
int main()
{
pid_t pid;
if((pid=fork())<0)
printf("fork error
");
else if(pid==0)
charatatime("output form child
");
else
charatatime("output form parent
");
exit(0);
}
static void charatatime(char *str)
{
char *ptr;
int c;
setbuf(stdout,NULL);
for(ptr=str;(c=*ptr++)!=0;)
putc(c,stdout);
}
程序中标准输出设置为不带缓冲的,每一次字符输出都需调用一次write。
修改程序,使用TEIL和WAIT函数。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
static void charatatime(char *);
int main()
{
pid_t pid;
TEIL_WAIT();
if((pid=fork())<0)
printf("fork error
");
else if(pid==0)
{
WAIT_PARENT();
charatatime("output form child
");
}
else{
charatatime("output form parent
");
TEIL_CHILD(pid);
}
exit(0);
}
static void charatatime(char *str)
{
char *ptr;
int c;
setbuf(stdout,NULL);
for(ptr=str;(c=*ptr++)!=0;)
putc(c,stdout);
}
函数exec:
用fork函数创建新的子进程后,子进程往往要调用一种exec函数以执行另一个程序。
#include <unistd.h>
int execl(const char *pathname,const char *arg0,...);
int execv(const char *pathname,char *const argv[]);
int execle(const char *pathname,const char *arg0,...);
int execve(const char *pathname,char *const argv[],char *const envp[]);
int execlp(const char *filename,const char *arg0,...);
int execvp(const char *filename,char *const argv[]);
int fexecve(int fd,char *const argv[],char *const envp[]);
更改用户ID和更改组ID:
可以使用:
#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
两个函数的返回值:成功,返回0.失败返回-1
getuid和geteuid函数只能获得实际用户ID和有效用户ID的当前值。我们没有可移植的方法
取获得保存的设置用户ID的当前值。
函数setreuid和setregid:
交换实际用户ID和有效用户ID的值:
#include <unistd.h>
int setreuid(uid_t ruid,uid_t euid);
int setregid(gid_t rgid,gid_t egid);
两个函数返回值:成功,返回0,出错,返回-1
若其中任一参数的值为-1,则表示相应的ID应当保持不变
函数seteuid和setegid:
只更改有效用户ID和有效组ID:
#include <unistd.h>
int seteuid(uid_t uid);
int setegid(gid_t gid);
解释器文件:
最常见的解释器文件:#!/bin/sh
函数system:
在程序中执行一个命令字符串很方便。例如,假定要将时间和日期放到某一个文件中,则可使用函数实现这一点。
调用time得到当前日历时间,接着调用localtime将日历时间变换为年、月、日、时、分、秒、周日的分解形式。
然后调用strftime对上面的结构进行格式化处理,最后写入到文件中。但是使用下列system函数更容易:
system("data > file");
#include <stdlib.h>
int system(const char *cmdstring);
如果cmdstring是一个空指针,则仅当命令处理程序可用时,system返回非0值,这一特征可以确定在一个给定的
操作系统上是否支持system函数。在UNIX中,system总是可用的。
因为system在实现中调用了fork、exec和waitpid函数,因此有3种返回值:
1、fork函数失败或者waitpid返回除EINTR之外的出错,则system返回-1,并且设置errno一指示错误类型。
2、如果exec失败(表示不能执行shell),则返回值 如同shell执行了exit(127)一样
3、否则所有3个函数(fork、exec和waitpid)都成功,那么system的返回值是shell的终止状态,其格式已在
waitpid说明。
下列程序是system函数的一种实现。它对信号没有进行处理。
#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>
int system(const char *cmdstring)
{
pid_t pid;
int status;
if(cmdstring==NULL)
return(1);
if((pid=fork())<0)
status=-1;
else if(pid==0)
{
execl("/bin/sh","sh","-c",cmdstring,(char *)0);
_exit(127);
}
else
{
while(waitpid(pid,&status,0)<0)
{
if(errno!=EINTR)
{
status=-1;
break;
}
}
}
return(status);
}
注释:system函数(没有对信号进行处理)
shell的-c选项告诉shell程序取下一个命令行参数(在这里是cmdstring)作为命令输入(而不是从标准输入或
从一个给定的文件中读命令)。shell对以null字节终止的命令字符串进行语法分析,并将它们分成命令行参数,传递
给shell的实际命令字符串可以包含任一有效的shell命令。例如,可以用<和>对输入和输出重定向。
#include "system.h"
//#include <sys.wait.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int status;
if((status=system("date"))<0)
printf("system() error
");
printf("status:%d
",status);
if((status=system("nosuchcommand"))<0)
printf("system() error
");
printf("status:%d
",status);
if((status=system("who;exit 44"))<0)
printf("system() error
");
printf("status:%d
",status);
exit(0);
}
打印实际用户ID和有效用户ID:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
printf("real uid=%d,effective uid=%d
",getuid(),geteuid());
exit(0);
}
进程会计:
函数(acct)启用和禁用进程会计。唯一使用这一函数的是accton(8)命令。超级用户执行一个带路径名参数的
accton命令启用会计处理。会计记录写到指定的文件中,在FreeBSD和Mac OS X中,该文件通常是/var/account/acct;
在linux中,该文件是/var/account/pacct;在Solaris中,该文件是/var/adm/pacct。执行不带任何参数的accton
命令则停止会计处理。
记录样式:
typedef u_short comp_t;
struct acct
{
char ac_flag;
char ac_stat;
uid_t ac_uid;
gid_t ac_gid;
dev_t ac_tty;
time_t ac_btime;
comp_t ac_utime;
comp_t ac_stime;
comp_t ac_etime;
comp_t ac_mem;
comp_t ac_io;
comp_t ac_rw;
char ac_comm[8];
}
会计记录所需的各个数据(各CPU时间、传输的字符数等)都由内核保存在进程表中,并在一个新进程被创建时初始化
(如fork之后在子进程中)。进程终止时写一个会计记录。这产生两个后果。
1、我们不能获取永远不终止的进程的会计记录。像init这样的进程在系统生命周期中一直在运行,并不产生会计记录。
这也同样适合于内核守护进程,它们通常不会终止。
2、在会计文件中记录的顺序对应于进程终止的顺序,而不是它们启动的顺序。为了确定启动顺序,需要读全部会计文件,
并按启动日历时间进行排序。这不是一种很完善的方法,因为日历时间的单位是秒,在一个给定的秒可能启动了多个进程。
而墙上时钟时间的单位是时钟滴答(通常,每秒滴答数在60~128)。但是我们并不知道进程的终止时间,所知道的只是启动
时间和终止顺序。这就意味着,即使墙上时钟时间比启动时间要精确得多,仍不能按照会计文件种的数据重构各进程的精确
启动顺序。
用户标识:
任一进程都可以得到实际用户ID和有效用户ID及组ID。但是,我们有时希望找到运行该程序用户的登录名。我们可以调用
getpwuid(getuid()),但是如果一个用户有多个登录名,这些登录名又对应着同一个用户ID,又将如何呢?(一个人在
口令文件中可以有多个登录项,它们的用户ID相同,但登录shell不同。)系统常常记录登录时使用的名字,用getlogin
函数可以获取此登录名:
#include <unistd.h>
char *getlogin(void);
返回值:成功,返回指向登录名字符串的指针;出错,返回NULL。
如果调用此函数的进程没有连接到用户登录时所用的终端,则函数会失败。通常称这些进程为守护进程。
给出了登录名,就可用getpwnam在口令文件中查找用户的相应记录,从而确定其登录shell等。
进程调度:
UNIX系统历史上对进程提供的只是基于调度优先级的粗粒度的控制。调度策略和调度优先级时由内核确定的。进程可以通过
调整nice值选择以更低优先级运行(通过调整nice值降低它对CPU的占有,因此进程时"友好的")。只有特权进程允许提供
调度权限。
nice值越小,优先级越高。NZERO是系统默认的nice值。
注意,定义NZERO的头文件因系统而异。除了头文件之外,Linux 3.2.0 可以通过非标准的sysconf参数(_SC_NZERO)来
访问NZERO的值。
进程可以通过函数获取或更改它的nice值。使用这个函数,进程只能影响自己的nice值,不能影响任何其他进程的nice值。
#include <unistd.h>
int nice(int incr);
返回值:成功,返回新的nice值NZERO;出错,返回-1.
incr参数被增加到调用进程的nice值上。如果incr太大,系统直接把它降到最大合法值,不给出提示。类似地,如果incr太
小,系统也会把它提高到最小合法值。由于-1是合法的成功返回值,在调用nice函数之前需要清除errno,在nice函数返回
-1时,需要检查它的值。如果nice调用成功,并且返回值为-1,那么errno仍然为0。如果errno不为0,说明nice调用失
败。
getpriority函数可以像nice函数那样用于获取进程的nice值,但是getpriority还可以获取一组相关进程的nice值:
#include <sys/resource.h>
int getpriority(int which,id_t who);
返回值:成功,返回-NZERO~NZERO-1之间的nice值;出错,返回-1
which参数可以取以下3个值之一:PRIO_PROCESS表示进程,PRIO_PGRP表示进程组,PRIO_USER表示用户ID。which
参数控制who参数是如何解释的,who参数选择感兴趣的一个或多个进程。如果who参数为0,表示调用进程、进程组或者用户
(取决于which参数的值)。当which设为PRIO_USER并且who为0时,使用调用进程的实际用户ID。如果which参数作用于
多个进程,则返回所有作用进程中优先级最高的(最小的nice值)。
setpriority函数可用于进程、进程组和属于特定用户ID的所有进程设置优先级。
#include <sys/resource.h>
int setpriorty(int which,id_t who,int value);
返回值,成功,返回0,失败,返回-1
参数which和who与getpriority函数相同。value增加到NZERO上,然后变为新的nice值。
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/resource.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=%11d
",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
",nzero);
if(argc==2)
adj=strtol(argv[1],NULL,10);
gettimeofday(&end,NULL);
end.tv_sec+=10;
if((pid=fork())<0)
printf("fork failed
");
else if(pid==0)
{
s="child";
printf("current nice value in child is %d,adjusting by %d
",
nice(0)+nzero,adj);
errno=0;
if((ret=nice(adj))==-1 && errno!=0)
printf("child set scheduling priority");
printf("now child nice value is %d
",ret+nzero);
}
else{
s="parent";
printf("current nice value in parent is %d
",nice(0)+nzero);
}
for(;;){
if(++count==0)
printf("%s counter wrap",s);
checktime(s);
}
}
执行:
./nice
结果:
NZERO =20
current nice value in parent is 20
current nice value in child is 20,adjusting by 0
now child nice value is 20
parent count= 133730266
child count= 133272218
./nice 20
结果:
NZERO =20
current nice value in parent is 20
current nice value in child is 20,adjusting by 20
now child nice value is 39
parent count= 243398034
child count= 3540318
进程时间:
墙上时钟时间、用户CPU时间和系统CPU时间。任一进程都可调用times函数获得它自己以及终止子进程的上述值:
#include <sys/times.h>
clock_t times(struct tms *buf);
返回值:成功,返回流逝的墙上时钟时间(以时钟滴答数为单位);出错,返回-1
此函数填写由buf指向的tms结构,该结构定义如下:
struct tms{
clock_t tms_utime;//user CPU time
clock_t tms_stime;//system CPU time
clock_t cutime;//user CPU time,terminated children
clock_t cstime;//system CPU time,terminated children
}
注意,此结构没有包含墙上时钟时间。times函数返回墙上时钟时间作为其函数值。此值时相对于过去的某一时刻度量的,
所以不能用其绝对值而必须使用其相对值。例如,调用times,保存其返回值。在以后再次调用times,从新返回的值中
减去以前返回的值,此差值就是墙上时钟时间。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/times.h>
#include <time.h>
static void pr_times(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("
command: %s
",cmd);
if((start=times(&tmsstart))==-1)
printf("times error
");
if((status=system(cmd))<0)
printf("system error
");
if((end=times(&tmsend))==-1)
printf("times error
");
pr_times(end-start,&tmsstart,&tmsend);
printf("status: %d
",status);
}
static void pr_times(clock_t real,struct tms *tmsstart,struct tms *tmsend)
{
static long clktck=0;
if(clktck==0)
{
if((clktck=sysconf(_SC_CLK_TCK))<0)
printf("sysconf error
");
}
printf("real: %7.2f
",real/(double)clktck);
printf("user: %7.2f
",(tmsend->tms_utime-tmsstart->tms_utime)/(double)clktck);
printf("sys: %7.2f
",(tmsend->tms_stime-tmsstart->tms_stime)/(double)clktck);
printf("child user: %7.2f
",(tmsend->tms_cutime-tmsstart->tms_cutime)/(double)clktck);
printf("child sys: %7.2f
",(tmsend->tms_cstime-tmsstart->tms_cstime)/(double)clktck);
}
执行:
./times "sleep 5" "date" "man bash > /dev/null"
结果:
command: sleep 5
real: 5.00
user: 0.00
sys: 0.00
child user: 0.00
child sys: 0.00
status: 0
command: date
Mon Mar 19 22:54:15 CST 2018
real: 0.04
user: 0.00
sys: 0.00
child user: 0.00
child sys: 0.00
status: 0
command: man bash > /dev/null
real: 0.76
user: 0.00
sys: 0.00
child user: 0.31
child sys: 0.15
status: 0