1.5程序和进程
1.5.1程序
程序是存放在磁盘文件中的可执行文件。
1.5.2进程和进程ID
程序的执行实例称为进程,每个UNIX进程都有一个唯一的指定的数字标示符,称为进程ID,进程ID总是一个飞负整数。
下面进程打印进程ID:
processid.c代码如下:
#include "ourhdr.h"
int main(void)
{
printf("hello world from process ID %d\n", getpid());
exit(0);
}
运行结果如下:
[root@localhost apue]# vim processid.c
[root@localhost apue]# gcc -o processid.out processid.c
[root@localhost apue]# ./processid.out
hello world from process ID 17503
1.5.3进程控制
有三个主要用于进程控制的函数:fork,exec,waitpid(exec有六种变体)。
下面的实例processct.c:
#include
#include
#include "ourhdr.h"
#include "error.c"
int main(void)
{
char buf[MAXLINE];
pid_t pid;
int status;
printf("%%");
while (fgets(buf,MAXLINE,stdin) != NULL){
buf[strlen(buf) - 1] = 0;
if ((pid = fork()) < 0)
err_sys("fork error");
else if (pid == 0) /*child*/
{
execlp(buf, buf, (char *)0);
err_ret("couldn't execute: %s", buf);
exit(127);
}
/*parent*/
if ((pid = waitpid(pid, &status, 0)) < 0)
err_sys("waitpid error");
printf("%% ");
}
exit(0);
}
fgets函数从标准输入一次读一行,当键入文件结束符(ctrl+d)作为一行的第一个字符时,fgets返回一个null指针,于是终止循环。
fork创建一个子进程fork对父进程返回子进程的非负进程ID,对子进程则返回0。
执行结果如下:
[root@localhost apue]# gcc -o processct.out processct.c
[root@localhost apue]# processct.out
-bash: processct.out: command not found
[root@localhost apue]# ./processct.out
%date
2013年 12月 25日 星期三 13:49:36 CST
% who
huangcd :0 2013-12-23 08:25
huangcd pts/1 2013-12-24 14:31 (:0.0)
% pwd
/apue
% [root@localhost apue]#
1.6 ANSI C
1.6.1 函数原型
头文件包含很多UNIX系统服务的函数原型,例如read,write和getpid函数等。函数原型是ANSI C标准的组成部分。
这些函数原型的格式如下:
ssize_t read(int, void *, size_t);
ssize_t write(int, const void *, size_t);
pid_t getpid(void);
提供了这些函数原型,编译程序在编译的时候就可以检查调用函数时是否使用了正确的参数。
1.6.2类属指针
从上面所示的函数原型中可以注意到另一个区别: r e a d和w r i t e的第二个参数现在是void *
类型。所有早期的U N I X系统都使用char *这种指针类型。作这种更改的原因是: ANSI C使用
void *作为类属指针来代替char *。
函数原型和类属指针的组合消去了很多非ANSI C编辑程序需要的显式类型强制转换。
例如,给出了w r i t e原型后,可以写成:
float data〔1 0 0〕;
write (fd, data, sizeof(dat;a))
若使用非A N S I编译程序,或没有给出函数原型,则需写成:
write(fd, (void *)data, sizeof(data));
也可将void *指针特征用于m a l l o c函数(见7 . 8节)。m a l l o c的原型为:
void * malloc(size_t);
这使得可以有如下程序段:
int * ptr;
ptr = malloc (1000 * sizeof(int));
它无需将返回的指针强制转换成int *类型。
1.6.3原始系统数据类型
getpid()函数原型定义了其返回值类型为pid_t类型,这也是POSIX中的新规定,还有read和write函数返回类型为ssize_t的值,并要求第三个参数是size_t类型。
以_t结尾的这些数据类型被称为原始系统数据类型,他们通常定义在头文件中定义(unistd.h头文件已包含该头文件)。
1.7出错处理
当UNIX系统出错的时候,通常返回一个负值,而且是通过整形变量errno返回,返回值通常被设置为一个具有特定信息的一个值。
文件中定义了变量errno以及可以赋给他的各种常数。通常都以E开头。
对于errno应当知道两条规则,第一条是:如果没有出错,则其值不会被一个例程清除。第二条:任一函数都不会将errno值设置为0。
C标准定义了两个函数,它们帮助打印出错信息。
#include
char *strerror(int er r n u m) ; //返回:指向消息字符串的指针
此函数将e rr n u m(它通常就是e r r n o值) 映射为一个出错信息字符串,并且返回此字符串的指针。
p e r r o r函数在标准出错上产生一条出错消息(基于e r r n o的当前值),然后返回。
#include
void perror(const char msg*) ;
它首先输出由m s g指向的字符串,然后是一个冒号,一个空格,然后是对应于e r r n o值的出
错信息,然后是一个新行符。
下面显示两个出错函数的使用方法:
errortest.c:
#include
#include "ourhdr.h"
int main(int argc, char *argv[])
{
fprintf(stderr, "EACCES:%s\n",strerror(EACCES));
errno = ENOENT;
perror(argv[0]);
exit(0);
}
执行结果:
[root@localhost apue]# gcc -o errortest.out errortest.c
[root@localhost apue]# ./errortest.out
EACCES:Permission denied
./errortest.out: No such file or directory
本书中的所有实例基本上都不直接调用s t r e r r o r或p e r r o r,而是使用附录B中的出错函数。该
附录中的出错函数使用了ANSI C的可变参数表设施,用一条C语句就可处理出错条件。
1.8用户标识
登录口令的文件中的用户ID是一个数值,它向系统标识各个不同的用户。
用户ID为0的用户为root管理员或者超级用户。超级用户对系统的大部分文件都有自由的支配权。
下面的程序用于打印用户的ID和组ID:
printId.c:
#include "ourhdr.h"
int main(void)
{
printf("uid = %d,gid = %d\n", getuid(), getgid());
exit(0);
}
执行结果如下:
[root@localhost apue]# gcc -o printId.out printId.c
[root@localhost apue]# ./printId.out
uid = 0,gid = 0
口令文件登录项也包括用户的组I D(group ID),它也是一个数值。组I D也是由系统管理
员在确定用户登录名时分配的。一般来说,在口令文件中有多个记录项具有相同的组I D。
组文件将组名映射为数字组I D,它通常是/ e t c / g r o u p。
对于许可权使用数值用户I D和数值组I D是历史上形成的。系统中每个文件的目录项包含该
文件所有者的用户I D和组I D。在目录项中存放这两个值只需4个字节(假定每个都以双字节的整
型值存放)。如果使用8字节的登录名和8字节的组名,则需较多的磁盘空间。但是对于用户而
言,使用名字比使用数值方便,所以口令文件包含了登录名和用户I D之间的映射关系,而组文
件则包含了组名和组I D之间的映射关系。例如UNIX ls-l命令使用口令文件将数值用户I D映射
为登录名,从而打印文件所有者的登录名。
除了在口令文件中对一个登录名指定一个组I D外,某些U N I X版本还允许一个用户属于另
外一些组。这是从4.2 BSD开始的,它允许一个用户属于多至1 6个另外的组。登录时,读文件
/ e t c / g r o u p,寻找列有该用户作为其成员的前1 6个登记项就可得到该用户的添加组I D
(supplementary group ID)。
1.9信号
信息是通知进程已发生某种条件的一种技术。例如,若某一进程执行除法操作,其除数为
0,则将名为S I G F P E的信号发送给该进程。进程如何处理信号有三种选择:
(1) 忽略该信号。有些信号表示硬件异常,例如,除以0或访问进程地址空间以外的单元等,
因为这些异常产生的后果不确定,所以不推荐使用这种处理方式。
(2) 按系统默认方式处理。对于0除,系统默认方式是终止该进程。
(3) 提供一个函数,信号发生时则调用该函数。使用这种方式,我们将能知道什么时候产
生了信号,并按所希望的方式处理它。
很多条件会产生信号。有两种键盘方式,分别称为中断键(interrupt key,通常是D e l e t e键
或C t r l - C )和退出键(quit key,通常是C t r l - \ ),它们被用于中断当前运行进程。另一种产生信号
的方法是调用名为k i l l的函数。在一个进程中调用此函数就可向另一个进程发送一个信号。当
然这样做也有些限制:当向一个进程发送信号时,我们必需是该进程的所有者。
程序1-8 从标准输入读命令并执行
readtest.c:
#include
#include
#include
#include "ourhdr.h"
#include "error.c"
static void sig_int(int);
int main(void)
{
char buf[MAXLINE];
pid_t pid;
int status;
if (signal(SIGINT, sig_int) == SIG_ERR)
err_sys("signal error");
printf("%%");
while (fgets(buf,MAXLINE,stdin) != NULL){
buf[strlen(buf) - 1] = 0;
if ((pid = fork()) < 0)
err_sys("fork error");
else if (pid == 0) /*child*/
{
execlp(buf, buf, (char *)0);
err_ret("couldn't execute: %s", buf);
exit(127);
}
/*parent*/
if ((pid = waitpid(pid, &status, 0)) < 0)
err_sys("waitpid error");
printf("%% ");
}
exit(0);
}
void sig_int(int signo)
{
printf("interrupt\n%% ");
}
执行结果:
[root@localhost apue]# gcc -o readtest.out readtest.c
[root@localhost apue]# ./readtest.out
%who
huangcd :0 2013-12-23 08:25
huangcd pts/1 2013-12-24 14:31 (:0.0)
% date
2013年 12月 25日 星期三 16:03:38 CST
% [root@localhost apue]# ./readtest.out
%who
huangcd :0 2013-12-23 08:25
huangcd pts/1 2013-12-24 14:31 (:0.0)
% fdfdf
couldn't execute: fdfdf: No such file or directory
% ^X
couldn't execute: : No such file or directory
% [root@localhost apue]#
1.10 UNIX时间值
长期以来,U N I X系统一直使用两种不同的时间值:
(1) 日历时间。该值是自1 9 7 0年1月1日0 0 : 0 0 : 0 0以来国际标准时间(U T C)所经过的秒数累
计值(早期的手册称U T C为格林尼治标准时间)。这些时间值可用于记录文件最近一次的修改
时间等。
(2) 进程时间。这也被称为C P U时间,用以度量进程使用的中央处理机资源。进程时间以
时钟滴答计算,多年来,每秒钟取为5 0、6 0或1 0 0个滴答。系统基本数据类型c l o c k t保存这种
时间值。另外, P O S I X定义常数C L K T C K,用其说明每秒滴答数。(常数C L K T C K现在已不
再使用。2 . 5 . 4节将说明如何用s y s c o n f函数得到每秒时钟滴答数。)
当度量一个进程的执行时间时(见3 . 9节),U N I X系统使用三个进程时间值:
• 时钟时间。
• 用户C P U时间。
• 系统C P U时间。
时钟时间又称为墙上时钟时间( wall clock time)。它是进程运行的时间总量,其值与系统
中同时运行的进程数有关。在我们报告时钟时间时,都是在系统中没有其他活动时进行度量的。
用户C P U时间是执行用户指令所用的时间量。系统C P U时间是为该进程执行内核所经历
的时间。例如,只要一个进程执行一个系统服务,例如r e a d或w r i t e,则在内核内执行该服务
所花费的时间就计入该进程的系统C P U时间。用户C P U时间和系统C P U时间的和常被称为
C P U时间。
要取得任一进程的时钟时间、用户时间和系统时间很容易——只要执行命令t i m e ( 1 ),其参
数是要度量其执行时间的命令,例如:
[root@localhost include]# time grep _POSIX_SOURCE */*.h >/dev/null
real 0m2.073s //估计这个时间包括进程等待的时间。从提交任务到任务完成的时间。
user 0m0.010s
sys 0m0.235s
1.11系统调用与库函数
所有的操作系统都提供多种服务的入口点,由此程序向内核请求服务。各种版本的U N I X
都提供经良好定义的有限数目的入口点,经过这些入口点进入内核,这些入口点被称为系统调
用(system call)。系统调用是不能更改的一种U N I X特征。U N I X第7版提供了约5 0个系统调用,
4 . 3 + B S D提供了约11 0个,而S V R 4则提供了约1 2 0个。
系统调用界面总是在《U N I X程序员手册》的第2部分中说明。其定义也包括在C语言中。
这与很多早期的操作系统不同,这些系统按传统方式在机器的汇编语言中定义内核入口点。
U N I X所使用的技术是为每个系统调用在标准C库中设置一个具有同样名字的函数。用户进
程用标准C调用序列来调用这些函数,然后,函数又用系统所要求的技术调用相应的内核服务。
例如函数可将一个或多个C参数送入通用寄存器,然后执行某个产生软中断进入内核的机器指
令。从应用角度考虑,可将系统调用视作为C函数。
《U N I X程序员手册》的第3部分定义了程序员可以使用的通用函数。虽然这些函数可能会
调用一个或多个内核的系统调用,但是它们并不是内核的入口点。例如, p r i n t f函数会调用
w r i t e系统调用以进行输出操作,但函数s t r c p y (复制一字符串)和a t o i (变换A S C I I为整数)并不使用
任何系统调用。
从执行者的角度来看,系统调用和库函数之间
有重大区别,但从用户角度来看,其区别并不非常
重要。在本书中系统调用和库函数都以C函数的形
式出现,两者都对应用程序提供服务,但是,我们
应当理解,如果希望的话,我们可以替换库函数,
但是通常却不能替换系统调用。
以存储器分配函数m a l l o c为例。有多种方法可
以进行存储器分配及与其相关的无用区收集操作(最
佳适应,首次适应等),并不存在对所有程序都最佳
的一种技术。U N I X系统调用中处理存储器分配的是
s b r k ( 2 ),它不是一个通用的存储器管理器。它增加
或减少指定字节数的进程地址空间。如何管理该地
址空间却取决于进程。存储器分配函数m a l l o c ( 3 )实
现一种特定类型的分配。如果我们不喜欢其操作方
式,则可以定义自己的m a l l o c函数,它可能将使用
s b r k系统调用。事实上,有很多软件包,它们实现
自己的存储器分配算法,但仍使用s b r k系统调用。
图1 - 1显示了应用程序、m a l l o c函数以及s b r k系统调
用之间的关系。
从中可见,两者职责不同,相互分开,内核中的系统调用分配另外一块空间给进程,而库
函数m a l l o c则管理这一空间。
另一个可说明系统调用和库函数之间的差别的例子是, U N I X提供决定当前时间和日期的
界面。某些操作系统提供一个系统调用以返回时间,而另一个则返回日期。任何特殊的处理,
例如正常时制和夏时制之间的转换,由内核处理或要求人为干预。U N I X则不同,它只提供一
条系统调用,该系统调用返回国际标准时间1 9 7 0年1月1日零点以来所经过的秒数。对该值的
任何解释,例如将其变换成人们可读的,使用本地时区的时间和日期,都留给用户进程运行。
在标准C库中,提供了若干例程以处理大多数情况。这些库函数处理各种细节,例如各种夏时
制算法。
应用程序可以调用系统调用或者库函数,而很多库函数则会调用系统调用。这在图1 - 2中
显示。
系统调用和库函数之间的另一个差别是:系统调用通常提供一种最小界面,而库函数通
常提供比较复杂的功能。我们从s b r k系统
调用和m a l l o c库函数之间的差别中可以看
到这一点,在以后当比较不带缓存的I / O函
数(见第3章)以及标准I / O函数(见第5章)
时,还将看到这种差别。
进程控制系统调用( fork, exec和w a i t)
通常由用户的应用程序直接调用(请回忆
程序1 - 5中的基本s h e l l)。但是为了简化某
些常见的情况,U N I X系统也提供了一些库
函数;例如s y s t e m和p o p e n。8 . 1 2节将说明
s y s t e m函数的一种实现,它使用基本的进
程控制系统调用。1 0 . 1 8节还将强化这一实
例以正确地处理信号。
为使读者了解大多数程序员应用的
U N I X系统界面,我们不得不既说明系统调
用,只介绍某些库函数。例如若只说明
s b r k系统调用,那么就会忽略很多应用程
序使用的m a l l o c库函数。
本书除了必须要区分两者时,都将使用术语函数( f u n c t i o n)来指代系统调用和库函数
两者。