一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着。
内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常退出则保存着导致该进程终止的信息是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量 $? 查看,因为Shell是它的父进程,当它终止时Shell调用 wait 或 waitpid得到它的退出状态同时彻底清除掉这个进程。
当一个进程正常或异常终止时,内核就向其父进程发送一个SIGCHLD信号。因为子进程终止是一个异步事件,所以发生这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数。对于这种信号的系统默认动作是忽略它。
上述叙述中提到了文件描述符、wait或waitpid,下面进行说明
1、文件描述符
三种标准输出流:stdin,stdout和stderr
#include<stdio.h>
extern FILE* stdin; extern FILE* stdout; externa FILE* stderr;
FILE*这种方式是由c语言(标准库)提供的;系统中任何一个程序跑起来都会默认得与这个程序关联3个标准输入输出流,分别是键盘、显示 器和显示器。
1)文件标识符:0表示标准输入,1表示标准输出,2表示标准错误。
下面程序进行说明
2)文件描述符是从0开始的一连串的小整数,程序运行起来后形成一个进程,通过进程找到文件,默认打开3个文件0,1,2。之后打开的文件,文件 描述符从3开始进行递增。如图所示
一旦关闭了某个文件描述符,则之后的文件描述符先为那个关闭文件描述符,接着从3开始递增。关闭0,即关闭标准输入,如图:
3)系统调用在库函数的下层,库函数调用系统函数。
2、wait()或waitpid()函数
头文件:
#include<sys/types.h>
#include<sys/wait.h>
等待函数:
1)pid_t wait(int *status);
返回值:成功返回被等待进程pid,失败返回-1
参数:输出型参数,获取进程退出状态,不关心则可以设置为NULL
如果进程由于接收到SIGCHLD而调用wait,则可期望wait会立即返回;但如果在任意时刻调用wait,则进程可能阻塞。
在一个子进程终止前, wait使其调用者堵塞,而waitpid有一个选项,可使调用者不堵塞。如果status不是空指针,则终止进程的终止状态就存放在它所指的单元内。如果不关心终止状态,则可将参数设置为空指针(waitpid同样适用)。
2)pid_t waitpid(int *status);
返回值:
a、当正常返回的时候waitpid返回收集到的子进程的进程id
b、如果设置了选项WNOHANG,而调用中waipid发现没有已退出的子进程可收集,则返回0
c、如果调用者出错,则返回-1,这是errno会被设置成相应的值以指示错误所在
d、当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD
参数:
a、pid(进程id)
pid=-1;等待任一个子进程,与wait等效。
pid >0;等待其进程ID与pid相等的子进程。
pid == 0;等待其组id等于进程组id的任一个子进程。
pid <-1;等待其组ID等于pid绝对值的任一个子进程。
b、status(int型)
status的高8位(WIFEXITED(status)) :若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
status的低8位 (WEXITSTATUS(status)) : 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
c、options(等待方式)
options为0:表示以阻塞方式进行等待,这种叫做阻塞式等待。
options为WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0(没有错误,也没有读取成功)不予以等待。若正常结 束,则返回该子进程的ID。
1 /*************************************************************************
2 > File Name: wait.c
3 > Author: ma6174
4 > Mail: ma6174@163.com
5 > Created Time: 2016年07月24日 星期日 19时57分23秒
6 ************************************************************************/
7
8 #include<stdio.h>
9 #include<sys/types.h>
10 #include<sys/wait.h>
11 #include<stdlib.h>
12
13 int main()
14 {
15 pid_t id = fork();
16 if(id < 0){
17 printf("fork failure\n");
18 }
19 else if(id == 0){
20 printf("child, pid:%d\n",getpid());
21 sleep(3);
22 //while(1);//会一直在等待子进程结束
23 printf("child deading...\n");
24 exit(0);
25 //exit(123);//子进程的退出码
26 //exit(257);//异常退出,发生溢出
27 }
28 else{
29 printf("father,pid:%d\n",getpid());
30 //pid_t ret = wait(NULL);//不关心进程退出状态
31 //int status = 0;
32 //pid_t ret = waitpid(id, &status,0);//0表示默认以阻塞方式进行等待
33 while(1){
34 pid_t ret = waitpid(id, NULL,WNOHANG);
35 printf("father wait return...\n");
36 sleep(1);
37 if(ret == 0){//进程没有结束
38 printf("child is running,Please wait\n");
39 }//等待成功或失败就退出循环
40 else if(ret = id){//wait child success
41 printf("wait success\n");
42 break;
43 //printf("code is done? %d\n",status&0xff);//查看进程是否正常退出
44 //printf("exit code? %d\n",(status>>8)&0xff);//查看退出码
45 }else{
46 printf("wait failed\n");
47 break;
48 }
49 sleep(2);//休眠2秒,不会一直等待
50 }
51 }
52
53 }<span style="color:#cc0000;">
</span>
运行结果如下:
3)检查wait和waitpid所返回的终止状态:
a、利用按位与,得到static的前8位和后8位
b、利用系统中的宏
WIFEXITED(status) : 若为正常终止子进程返回的状态,则为真。
WEXITSTATUS(status) : 若WIFEXITED非零,返回子进程退出码,提取进程退出返回值,如果子进程调exit(7),WEXITSTATUS(status就会返 回7。请注意,如果进程不是正常退出的,也就是说,WIFEXITED返回0,这个值就毫无意义。
说明:status 并不简简单单是一个整形变量,父进程和子进程之间所有的状态交互都要通过这个int来表示,所以这个int的若干bit位都是有特殊的含义的,那么这个“int”如何编码就比较重要了,和IP地址一样,它是比较紧凑的,或者说是比较拥挤的。
status指出了子进程是正常退出还是被非正常结束的(一个进程也可以被其他进程用信号结束),以及正常结束时的返回值,或被哪一个信号结束或进程的退出码是多少等信息,这些信息都被放在整数的不同二进制位中,所以用常规的方法读取会非常麻烦,所以开发者就设计了一套专门的宏(macro)来完成这项工作。
4)wait和waitpid的作用:
a、等待目的:子进程读取到子进程退出的一个状态信息
b、让系统释放掉子进程占有的僵尸状态的资源
c、运行时不保证父子进程谁先运行,可保证子进程先退出,等待使父子进程的退出同步起来(即退出产生一定的顺序)
5)waitpid提供了wait没有提供的功能:
a、waitpid可等待一个特定的进程
b、waitpid提供了一个wait的非阻塞版本
c、waitpid支持作业控制
3、进程程序替换
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
1)其实有6种以exec开头的函数,统称为exec函数:
#incldue<unistd.h>
int execl(const char* path,const char* arg,....);
int execlp(const char* file,const char* arg,....);
int execle(const char* path,const char* arg,..., char* const emp[]);
int execv(const char* path,char* const argv[]);
int execp(const char* file,char* const argv[]);
int execve(const char* path,char* const argv[],char* const emp[]);
这些函数如果调用成功,则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。
2)这些函数的规则:
不带字母p (表示path)的exec函数 第一个参数必须是程序的相对路径或绝对路径,例如"/bin/ls"或"./a.out",而不能 是"ls"或"a.out"。对于带字母p的函数: 如果参数中包含/,则将其视为路径名。 否则视为不带路径的程序名,在PATH环境变量的目录列表中搜索这个程序。
带有字母l( 表示list)的exec函数要求将新程序的每个命令行参数都当作一个参数传给它,命令行参数的个数是可变的,因此函数原型中有...,...中的最后一个可变参数应该是NULL, 起sentinel的作用。
带有字母v( 表示vector)的函数,则应该先构造一个指向各参数的指针数 组,然后将该数组的首地址当作参数传给它,数组中的最后一个指针也应该是NULL,就像main函数的argv参数或者环境变量表一样。
对于以e (表示environment)结尾的exec函数,可以把一份新的环境变量表传给它,其他exec函数仍使用当前的环境变量表执行新程序。
上述进程用程序替换执行如下:
Linux下的open()、close()、write()和read()函数
1. open()函数
功能描述:用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。
所需头文件:#include <sys/types.h>,#include <sys/stat.h>,#include <fcntl.h>
函数原型:int open(const char *pathname,int flags,int perms)
参数:
pathname:被打开的文件名(可包括路径名如"dev/ttyS0")
flags:文件打开方式,
O_RDONLY:以只读方式打开文件
O_WRONLY:以只写方式打开文件
O_RDWR:以读写方式打开文件
O_CREAT:如果改文件不存在,就创建一个新的文件,并用第三个参数为其设置权限
O_EXCL:如果使用O_CREAT时文件存在,则返回错误消息。这一参数可测试文件是否存在。此时open是原子操作,防止多个进程同时创建同一个文件
O_NOCTTY:使用本参数时,若文件为终端,那么该终端不会成为调用open()的那个进程的控制终端
O_TRUNC:若文件已经存在,那么会删除文件中的全部原有数据,并且设置文件大小为0
O_APPEND:以添加方式打开文件,在打开文件的同时,文件指针指向文件的末尾,即将写入的数据添加到文件的末尾
O_NONBLOCK: 如果pathname指的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选择项为此文件的本次打开操作和后续的I/O操作设置非阻塞方式。
O_SYNC:使每次write都等到物理I/O操作完成。
O_RSYNC:read 等待所有写入同一区域的写操作完成后再进行
在open()函数中,falgs参数可以通过“|”组合构成,但前3个标准常量(O_RDONLY,O_WRONLY,和O_RDWR)不能互相组合。
perms:被打开文件的存取权限,可以用两种方法表示,可以用一组宏定义:S_I(R/W/X)(USR/GRP/OTH),其中R/W/X表示读写执行权限,
USR/GRP/OTH分别表示文件的所有者/文件所属组/其他用户,如S_IRUUR|S_IWUUR|S_IXUUR,(-rex------),也可用八进制800表示同样的权限
返回值:
成功:返回文件描述符
失败:返回-1
2. close()函数
功能描述:用于关闭一个被打开的的文件
所需头文件: #include <unistd.h>
函数原型:int close(int fd)
参数:fd文件描述符
函数返回值:0成功,-1出错
3. read()函数
功能描述: 从文件读取数据。
所需头文件: #include <unistd.h>
函数原型:ssize_t read(int fd, void *buf, size_t count);
参数:
fd: 将要读取数据的文件描述词。
buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。
count: 表示调用一次read操作,应该读多少数量的字符。
返回值:返回所读取的字节数;0(读到EOF);-1(出错)。
以下几种情况会导致读取到的字节数小于 count :
A. 读取普通文件时,读到文件末尾还不够 count 字节。例如:如果文件只有 30 字节,而我们想读取 100
字节,那么实际读到的只有 30 字节,read 函数返回 30 。此时再使用 read 函数作用于这个文件会导致 read 返回 0 。
B. 从终端设备(terminal device)读取时,一般情况下每次只能读取一行。
C. 从网络读取时,网络缓存可能导致读取的字节数小于 count字节。
D. 读取 pipe 或者 FIFO 时,pipe 或 FIFO 里的字节数可能小于 count 。
E. 从面向记录(record-oriented)的设备读取时,某些面向记录的设备(如磁带)每次最多只能返回一个记录。
F. 在读取了部分数据时被信号中断。
读操作始于 cfo 。在成功返回之前,cfo 增加,增量为实际读取到的字节数。
4. write()函数
功能描述: 向文件写入数据。
所需头文件: #include <unistd.h>
函数原型:ssize_t write(int fd, void *buf, size_t count);
返回值:写入文件的字节数(成功);-1(出错)
功能:write 函数向 filedes 中写入 count 字节数据,数据来源为 buf 。返回值一般总是等于 count,否则就是出错了。常见的出错原因是磁盘空间满了或者超过了文件大小限制。
对于普通文件,写操作始于 cfo 。如果打开文件时使用了 O_APPEND,则每次写操作都将数据写入文件末尾。成功写入后,cfo 增加,增量为实际写入的字节数。