CPU 执行一条指令过程:预取器先从cache或者内存中取出一条指令,然后交给译码器分析,译码器分析该条指令需要用到哪个寄存器,并把相关数据存储到对应的寄存器中,ALU对其进行运算,然后把数据回写到寄存器中,最后再把数据放到缓冲区,然后由内存把数据传输到总线,再显示到设备上
虚拟地址:可用地址空间0-4G
假如虚拟地址使用了2KB,那么映射到物理内存大小应该为4KB,因为一个page作为物理内存的最小单位,大小为4KB。
每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。
/usr/src/linux-headers-3.16.0-30(可能不一样)/include/linux/sched.h文件中可以查看struct task_struct 结构体定义。
其内部成员有很多,重点掌握以下部分即可:
ulimit -a
环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。
通常具备以下特征:
名=值[:值]
存储形式:与命令行参数类似。char *[]数组,数组名environ,内部存储字符串,NULL作为哨兵结尾。
使用形式:与命令行参数类似。
加载位置:与命令行参数类似。位于用户区,高于stack的起始位置。
引入环境变量表:须声明环境变量。extern char ** environ;
按照惯例,环境变量字符串都是name=value这样的形式,大多数name由大写字母加下划线组成,一般把name的部分叫做环境变量,value的部分则是环境变量的值。环境变量定义了进程的运行环境,一些比较重要的环境变量的含义如下:
PATH
可执行文件的搜索路径。ls命令也是一个程序,执行它不需要提供完整的路径名/bin/ls,然而通常我们执行当前目录下的程序a.out却需要提供完整的路径名./a.out,这是因为PATH环境变量的值里面包含了ls命令所在的目录/bin,却不包含a.out所在的目录。PATH环境变量的值可以包含多个目录,用:号隔开。在Shell中用echo命令可以查看这个环境变量的值:echo $PATH
SHELL
当前Shell,它的值通常是/bin/bash。echo $SHELL
TERM
当前终端类型,在图形界面终端下它的值通常是xterm,终端类型决定了一些程序的输出显示方式,比如图形界面终端可以显示汉字,而字符终端一般不行。
LANG
语言和locale,决定了字符编码以及时间、货币等信息的显示格式。
HOME
当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。
练习:打印当前进程的所有环境变量。
#include
#include
using namespace std;
extern char **environ;//须声明环境变量
int main(){
for(int i = 0;environ[i];i++){
cout << environ[i] << endl;
}
return 0;
}
#include
#include
#include
using namespace std;
int main(){
const char* name = "ABD";
char *val;
val = getenv(name);//获取环境变量ABD的值,此时环境变量不存在,所以为空
if(val){
cout << name << ": " << val << endl;
}else{
cout << "环境变量不存在" << endl;
}
setenv(name,"day-day-up",1);//设置环境变量的值为day-day-up
val = getenv(name);
cout << name << ": " << val << endl;
int ret = unsetenv("ABCD");//删除环境变量的定义
cout << "环境变量ABCD 的值为:" << ret << endl;
ret = unsetenv("ABD");
cout << "环境变量ABD的值为:" << ret << endl;
cout << getenv(name) << endl;
return 0;
}
函数 | 函数原型 | 说明 |
---|---|---|
getpid函数 | pid_t getpid(void); |
获取当前进程ID |
getppid函数 | pid_t getppid(void); |
获取当前进程的父进程ID |
getuid函数 | uid_t getuid(void); |
获取当前进程实际用户ID |
geteuid()函数 | uid_t geteuid(void); |
获取当前进程有效用户ID |
getgid函数 | gid_t getgid(void); |
获取当前进程使用用户组ID |
getegid函数 | gid_t getegid(void); |
获取当前进程有效用户组ID |
区分一个函数是“系统函数”还是“库函数”依据:
fork函数:创建一个子进程
pid_t fork(void);
#include
#include
#include
using namespace std;
int main(){
cout << "testtesttesttest!!!" << endl;
pid_t pid = fork();
if(pid == -1){
perror("fork");
exit(1);
}else if(pid == 0){
cout << "此进程为子进程" << endl;
cout << "父进程pid为:" << getppid() << endl;
cout << "子进程pid为:" << getpid() << endl;
}else{
cout << "此进程为父进程" << endl;
cout << "父进程pid为:" << getppid() << endl;
cout << "子进程pid为:" << getpid() << endl;
sleep(1);
}
cout << "hellohellohello!!!" << endl;
return 0;
}
ps aux | grep 22865
使用for(i = 0; i < n; i++) { fork(); }
创建的子进程:
//循环创建5个子进程
#include
#include
#include
using namespace std;
int main(){
cout << "创建5个子进程" << endl;
pid_t pid;
int i;
for(i = 0;i < 5;i++){
pid = fork();
if(pid == 0){//子进程就跳出此循环,防止为子进程创建进程
break;
}
}
if(i < 5){//子进程,break跳出后执行后续程序
sleep(i);//保证进程输出的顺序
cout << "I am " << i + 1 << " child fork!!!" << endl;
}else{ //父进程
sleep(i);
cout << "I am parent fork!!!" << endl;
}
return 0;
}
一次循环结束后,父进程会执行下一次循环,子进程会跳出循环执行循环后的内容,父子进程执行先后顺序取决于谁先抢到CPU资源,并不能保证其执行顺序(一般是父进程先执行,但是没有理论依据),如下图(注释程序中sleep(i)后的运行结果,其中shell进程也参与cpu资源争夺)。
父子进程遵循原则:读时共享写时复制
刚fork之后:
父子相同处: 全局变量(不能共享)、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…
父子不同处:
似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?
当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
【重点】:父子进程共享:
使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过指令设置gdb调试工具跟踪父进程或者是跟踪子进程。默认跟踪父进程。
set follow-fork-mode child
命令设置gdb在fork之后跟踪子进程。set follow-fork-mode parent
设置跟踪父进程。注意,一定要在fork函数调用之前设置才有效
fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
将当前进程的.text、.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。
其实有六种以exec开头的函数,统称exec函数:
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 envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
加载一个进程,借助PATH环境变量
int execlp(const char *file, const char *arg, ...);
//实现ls -la
#include
#include
#include
#include
using namespace std;
int main(){
pid_t pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid > 0){
sleep(2);
cout << "father fork" << endl;
}else{
//execlp函数
execlp("ls","ls","-l","-a",NULL);//实现ls -la
}
return 0;
}
加载一个进程, 通过 路径+程序名 来加载。可利用子进程执行自己写的程序
int execl(const char *path, const char *arg, ...);
//实现ls -la
#include
#include
#include
#include
using namespace std;
int main(){
pid_t pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid > 0){
sleep(2);
cout << "father fork" << endl;
}else{
//execl函数
execl("/bin/ls","ls","-l","-a",NULL);//实现ls -la
//execl("./hello.cpp","hello",NULL);//运行hello.cpp文件
}
return 0;
}
exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常我们直接在exec函数调用后直接调用perror()和exit(),无需if判断。
命令行方式:ps aux > 文件
#include
#include
#include
#include
using namespace std;
int main(){
int fd = open("out.txt",O_WRONLY|O_CREAT|O_TRUNC,0644);
if(fd < 0){
perror("open error");
exit(1);
}
dup2(fd,STDOUT_FILENO);//dup2(3,1),3相当于打开的文件:out.txt,而1是标准输出standout
execlp("ps","ps","aux",NULL);
return 0;
}
父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。
#include
#include
#include
using namespace std;
int main(){
pid_t pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid > 0){
cout << "I am parent,my pid is " << getpid() << endl;
sleep(8);
cout << "----------- parent going to die -------------"<< endl;
}else{
while(1){
cout << "I am child,my parent pid is " << getppid() << endl;
sleep(1);
}
}
return 0;
}
子进程每隔一秒打印一次,父进程在八秒后结束,此时子进程还没结束,成为孤儿进程
僵尸进程: 进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
特别注意,僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。
#include
#include
#include
using namespace std;
int main(){
pid_t pid = fork();
if(pid == 0){
cout << "child ,my father pid is " << getppid() << endl;
sleep(8);
cout << "--------child die -------" << endl;
}else if(pid > 0){
while(1){
cout << "I am father,my pid is " << getpid() << " myson pid is " << pid << endl;
sleep(1);
}
}else{
perror("fork error");
exit(1);
}
return 0;
}
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。
这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:
pid_t wait(int *status);
成功:清理掉的子进程ID;失败:-1 (没有子进程)
当进程终止时,操作系统的隐式回收机制会:1.关闭所有文件描述符 2. 释放用户空间分配的内存。内核的PCB仍存在。其中保存该进程的退出状态。(正常终止→退出值;异常终止→终止信号)
可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
1. WIFEXITED(status) 为非0 → 进程正常结束
WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)
2. WIFSIGNALED(status) 为非0 → 进程异常终止
WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。
3. WIFSTOPPED(status) 为非0 → 进程处于暂停状态
WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。
WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行
#include
#include
#include
using namespace std;
int main(){
pid_t pid = fork();
int status;//wait(&status)
if(pid == 0){
cout << "child ,my father pid is " << getppid() << endl;
sleep(30);
cout << "--------child die -------" << endl;
exit(66);
}else if(pid > 0){
// pid_t wpid = wait(NULL); //一般回收
pid_t wpid = wait(&status);
if(wpid == -1){
perror("wait error");
exit(1);
}
//正常结束
if(WIFEXITED(status)){
cout << "child exit with " << WEXITSTATUS(status) << endl;
}
//异常终止
if(WIFSIGNALED(status)){
cout << "child killed by " << WTERMSIG(status) << endl;
}
while(1){
cout << "I am father,my pid is " << getpid() << " myson pid is " << pid << endl;
sleep(1);
}
}else{
perror("fork error");
exit(1);
}
return 0;
}
打开另一个终端窗口,手动发送信号 9 杀死子进程,此时子进程异常终止,返回终止信号 9。
作用同wait,但可指定pid进程清理,可以不阻塞。
pid_t waitpid(pid_t pid, int *status, in options);
特殊参数和返回情况:
waitpid(-1,NULL,0); 相当于 wait(NULL);