WARNING这是《深入理解计算机系统》(CS
)的笔记,彼时还未上CO,OS等计算机系统基础课,存在理解不到位与错误之处.
Unix提供了大量C程序中操作进程的系统调用,这里介绍其中几个重要的函数。为难的是这些函数对应的头文件比较混乱,并且很多在windows下不兼容。
系统调用错误处理
在正式开始介绍进程操作的系统调用前,先看一看系统调用函数的错误处理与包装。系统调用中某个库定义了全局整数变量 errno,当Unix系统级函数出错时,它通常会返回-1。通过 errno 的值可以解码出定义过的几个错误类型,使用 strerror() 函数可以对这串数字解码,返回和它关联的错误字符串。(我暂不确定这是不是库函数)。
为了稳定,必须由错误处理成分,虽然系统调用出错的概率看起来很小。
比如,对常用的 fork() 函数的含错误处理的包装:
/* 未包装版本 */
if((pid=fork())<0){ //fork()出错返回-1
fprintf(stderr,"fork error: %s\n",strerror(errno));
exit(0); //终止此进程.
}
/* 含错误处理包装 */
/* 首先是更普适的错误表达函数,unix风格 */
void unix_error(char *msg) //msg是在调用什么的时候出错
{
fprintf(stderr,"%s: %s\n",msg,strerror(errno));
exit(0);
}
/* 包装的fork:Fork() */
pid_t Fork(void)
{
pid_t pid;
if((pid=fork())<0){
unix_error("fork error");
}
return pid;
}
通过这样简单方式包装的函数使得代码简化、可读性更高,用原函数首字母大写表示新的包装函数(比如fork—>Fork)。
获取进程ID (PID)
每个进程都有一个唯一的识别ID (PID),用 getpid 返回本进程 pid,用 getppid 返回父进程 pid。Linux下每个进程都一定至少有父进程 _init。这和它运行程序的过程有关系。在《链接概述》中有稍微提及。
#include<sys/types.h>
#include<unistd.h>
pid_t getpid(void);
pid_t getppid(void);
//Linux下pid_t被定义为int。
终止进程
从程序员角度,进程只有3种状态:运行,停止(挂起),终止。后两者都可能是某种异常或者信号所致,也可以通过系统调用主动控制。用 exit() 函数主动终止进程。(另一种终止方法是从main程序中 return 一个整数值)。
#include<stdlib.h>
void exit(int status);
以status退出状态来终止进程。
创建子进程
用 fork() 函数创建子进程,创建出来的子进程最开始其实是对父进程拷贝,有相同的上下文,共用同一个代码文件。从这个函数开始,进程控制函数都比较微妙。fork() 函数调用一次,返回两次。注意返回两次,是一次返回到原进程中相同位置,另一次返回到子进程中相同位置,注意先返回到那个进程中是不确定的!。
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
//Windows下不支持。若出错返回-1
//返回到原本进程的时候,返回值为所创建的子进程的pid。
//返回到子进程的时候,返回值为0.
通过返回值可以判断自己处于子进程还是父进程。
int main()
{
pid_t pid;
int x=1;
pid=Fork();
if(pid==0){ //返回到子进程
printf("child: %d\n",++x);
exit(0);
}
printf("parent: %d\n",--x);
exit(0);
}
运行后,可能的结果为:
child: 2
parent: 0
或者
parent: 0
child: 2
父子进程由相同的上下文环境,因而都有等值的全局变量x。但是父子进程是不同进程,对自己的x的改变不影响另一个进程。同样,子进程继承了父进程的文件打开状态,父进程打开了stdout,子进程也就打开了stdout,因而子进程的printf也输出到同一个屏幕上。
另外可以看到,fork先返回到父进程还是子进程时随机不可控的。
通过对返回值的判断,可以让程序知道是在子进程中还是父进程中。
当fork比较多的时候,可以画进程图 辅助思考。比如:
int main()
{
Fork();
Fork();
printf("hello\n");
exit(0);
}
一共有四行hello。
回收子进程(P517)
当子进程终止,内核并不立刻将其回收清理,而是保持为终止的状态,等父进程回收它。此过程中,这个终止的子进程被称为僵死进程(Zombie) 。
子进程实际上可以脱离父进程独立存在。当子进程未终止而父进程终止的时候,内核会安排 init 进程为它的养父,由 init 回收它。init在系统启动的时候创建,是所有进程的祖先,不会停止。长时间运行的进程总该试图回收它的子进程,僵死进程将消耗系统资源。
用waitpid()函数和它的简化wait() 函数回收子进程。
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int *statusp, int options);
pid_t wait(int *statusp);
waitpid 中,pid 参数是等待集合,options 是默认行为,statusp 用来检查退出状态。
默认情况下(options=0),waitpid挂起调用它的进程,直到等它的等待集合中的一个子进程终止;如果调用的时候已经有子进程终止,则立刻返回。这种状况下,waitpid回收这个终止的子进程,并返回它的pid,将 statusp设置为该子进程的退出状态。
出错则返回-1.
1)等待集合pid
如果pid>0,则只等这个特定的子进程。
如果pid=-1,等待集合为该进程的所有子进程。
waitpid还包括其它等待集合,比如进程组。
2)修改默认行为 options
options可以修改为常量 WNOHANG WUNTRACED WCONTINUE.
WNOHANG 试探一下,如果等待集合中没有子进程结束,则立即返回0,不等。
WUNTRACED 挂起调用进程,当等待集合中有成员终止或者停止,则返回。
WCONTINUE 等等待集合中有一个终止,或者有一个已经停止的成员收到 SIGCONT 信号重新开始执行,则返回。
可以对它们用或运算组合:
WNOHANG | WUNTRACED 没有子进程停止或终止的时候,立刻返回。
3)检查已回收子进程的退出状态。
通过 status(statusp 指向的值) 的值检查。wait.h 中定义了几个解释退出状态的宏函数:
WIFEXITED(status):如果通过exit 或return正常退出,这个宏函数返回1.
WEXITSTATUS(status):返回正常终止的子程序的退出状态。就是exit(int status)、return xx 中的那个值。必须先调用上一个宏函数。
WIFSIGNALED(status):是被一个未被捕获的信号终止的,就返回真。
WTERMSIG(status):返回导致停止的信号的编号。必须先调用上一个宏函数。
WIFSTOPPED(status):引起返回的子程序当前是停止(而非终止)的。
WSTOPSIG(status):引起停止的信号编号。
WIFCONTINUE(status):引起返回的子进程是收到SIGCONT信号继续执行。
4)错误条件:
出错返回-1。如果没有子进程,设置 errno 为ECHILD 。如果waitpid被信号中断,设置 errno 为 EINTR。
wait(stautsp); == waitpid(-1, statusp, 0);。
进程休眠
#include<unistd.h>
int pause(void); //让函数休眠,直到收到一个信号
unsigned int sleep(unsigned int secs); //有可能中途被信号打断sleep。返回还剩下未sleep的时间。
加载、运行程序
使用execve()保留 当前进程的所有上下文,并且加载新程序并执行。
#include<unistd.h>
int execve(const char *filename, const char *argv[], const char *envp[]);
加载运行目标文件,并有命令行和环境变量。
主函数的完整定义是:
int main(int argc, char *argv[], char *envp[]);
环境变量的格式为:条目name=值、字符串
有几个操作环境变量的函数:
#include<stdlib.h>
char *getenv(const char *name); //查找是否有条目name,若有,返回value
int setenv(const char *name,const char *newvalue, int overwrite);
//若已有name,当overwrite!=0,用newvalue覆盖掉。如果无,则加上。
void unsetenv(const char *name); //删除
有了这些函数,已经可以写一个超级简化版的my_shell了。