进程
进程是一个执行中的程序的实例。系统中的每个程序都是运行在某个进程的上下文中。上下文由程序正确运行所需的状态组成。这个状态包括,存放在存储器中的代码和数据,它的栈,通用目的寄存器内容,程序计数器,环境变量,以及打开文件描述符的集合。
每次用户向外壳(shell)运行一个可执行目标文件,shell会创建一个新的进程,然后再这个新进程的上下文中运行这个程序。应用程序也可以自己创建进程,并在创建的新进程中运行自己的代码或其他应用程序。
私有地址空间
进程为每个应用程序提供一个假象,好像它独占地使用系统地址空间。在一台有n位地址的机器上,地址空间是2n个可能地址的集合,0 ~ 2n-1。一般而言,和这个进程地址空间中的某个地址相关联的存储器字节,是不能被其他进程读写的。(肯定啊)从这个意义上说,这个地址空间是私有的。
地址空间的顶部(?~ 2n-1)是保留给内核的。这个部分包含内核的代码、数据、堆、栈等。
用户模式和内核模式
通过某个控制寄存器的一个模式位(mode bit),来控制进程运行在哪个模式。
运行应用程序代码的进程初始时是在用户模式中。进程从用户模式变为内核模式的唯一方法是通过中断、故障或者陷入陷阱进行系统调用。
Linux的/proc文件系统。它允许用户模式进程访问内核数据结构的内容。/proc文件系统将许多内核数据结构的内容输出为一个用户程序可以读的文本文件的层次结构。比如/proc/cpuinfo查看cpu类型,/proc//maps查看进程使用的存储器段。
上下文切换
发生时机
当程序切换到内核模式执行系统调用时,可能发生。如果系统调用因为某个等待的时间发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。比如,一个read系统调用请求一个磁盘访问,内核可以选择执行上下文切换,运行另一个进程,而不是等待数据从磁盘到达。另一个示例是sleep系统调用,它显式的请求让调用进程休眠。
中断也可能引发上下文切换。每次发生定时器中断时,内核就判定当前进程已经运行了足够长的时间,并切换到一个新的进程。
中断处理程序/上下文切换 污染高速缓存
中断处理程序如果访问了足够多的表项,那么再切换回应用程序时,缓存是冷的。上下文切换也会出现同样的情况。(那怎么办?)
系统调用错误处理
当Unix系统级函数遇到错误时,它们会典型地返回-1,并设置全局整数变量errno来表示出了什么错。可以用
strerror(errno)来返回errno此时相关联的错误。
进程控制
进程ID
每个进程都有一个唯一的正数ID,称为PID。
可以用getpid()来获取。getppid()可以返回它父进程的PID。
创建、终止进程。
相同、独立的地址空间。父子进程拥有相同的用户栈、相同的本地变量值、相同的堆、相同的全局变量值、以及相同的代码。子进程得到与父进程用户级虚拟地址空间相同,但是独立的一份拷贝。包括文本、数据和bss段、堆以及用户栈。对比一下第七章讲的进程存储器映像,除下内核地址空间和共享库,其他都拷贝了。
子进程还获得和父进程任何打开的文件描述符相同的拷贝。这就意味着调用fork后,子进程可以读写fork之前父进程打开的任何文件。
阻塞,进程的执行被暂时挂起(suspend)。当收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU信号时,进程就会阻塞挂起,直到它收到一个SIGCONT信号才会继续。信号是一种软件中断的形式。
终止,进程永远停止。进程会因为三种原因终止:
收到一个信号,信号的默认行为是终止进程。(比如: kill -9)
从主程序(main)返回
- 调用exit函数。exit函数以status退出状态来终止进程(另一种设置退出状态的是从主程序返回一个数值)
回收子进程
僵死进程
当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程会保持在一种已终止的状态中,知道被它的父进程回收。当父进程回收已终止的子进程时,内核将子进程的退出状态(上一小节说的exit status?)传递给父进程,然后抛弃已终止的进程,从此时开始,该进程就不存在了。一个终止了,但还未被回收的进程称为僵死进程。
如果父进程没有回收它自己的僵死子进程就终止了,那么内核会安排init进程来回收它们。init进程PID为1,并且是由系统初始化时内核创建的。
等待子进程结束
1. waitpid(pid_t pid, int *status, int options)
2. wait(int *status) ,相当于调用waitpid(-1, &status, 0)
休眠
#include <unistd.h>
unsigned int sleep(unsigned int secs);
如果请求的时间量到了,sleep返回0,否则返回还剩下的要休眠的秒数。后一种情况,可能发生在sleep函数被一个信号中断而提前返回的情况。
#include <unistd.h>
int pause(void);
pause()函数使调用者休眠,直到该进程收到一个信号。
加载并运行程序,execve
#include <unistd.h>
int execve(const char *filename, const char *argv[], const char *envp[]);
execve函数加载并运行可执行文件filename,且带参数列表argv和环境变量列表envp。execve调用一次从不返回,除非出现错误。main函数也是三个参数,不过envp是隐藏的默认参数。
int main(int argc, char *argv[], char *envp[]);
用户栈典型结构,注意啦,复习下进程的存储器映像,栈是从高地址向地地址分配,所以栈底的地址比栈顶大。argv和envp,都是以一个null元素结尾,因此即使不知道argv和envp的长度,也可以依次打印出来,而避免越界。
几个操作envp的函数
#include <stdlib.h>
char *getenv(const char *name);
int setenv(const char *name, const char *newvalue, int overwrite);
void unsetenv(const char *name);
fork和execve
区别
fork函数在新的子进程中运行相同的程序,新的子进程时父进程的一个复制品。execve函数在当前进程的上下文中运行一个新的程序。它会覆盖当前进程的地址空间,但是并没有创建一个新进程。新的程序仍然有相同的PID,并且继承了调用execve函数时,已打开的所有文件描述符。
利用fork和execve运行程序
shell进程就是这样来执行命令。简单版如下,再结合waitpid就可以实现相应的后台执行等功能。
if(pid = fork() == 0)
{
if(execve(argv[0], argv, environ) < 0)
{
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}