CPU和MMU
进程控制块PCB(重点!!!)
我们知道,每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。
/usr/src/linux-headers-3.16.0-30/include/linux/sched.h文件中可以查看struct task_struct 结构体定义。其内部成员有很多,我们重点掌握以下部分即可:
- 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
- 进程的状态,有就绪、运行、挂起、停止等状态。
- 进程切换时需要保存和恢复的CPU中的一些寄存器的值(寄存器是在CPU中公用的,而进程切换后这些寄存器又会被另一个进程所使用,所以要保护现场,即把当前进程的寄存器的值存到PCB中)。
- 描述虚拟地址空间的信息。(页表)
- 描述控制终端的信息。(ps指令可以查看终端信息)
- 当前进程的工作目录(Current Working Directory)。
- umask掩码。
- 文件描述符表,包含很多指向file结构体的指针。
- 和信号相关的信息。
- 用户id和组id。
- 会话(Session)和进程组。
- 进程可以使用的资源上限(Resource Limit)。
环境变量:
环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。通常具备以下特征:
① 字符串(本质) ② 有统一的格式:名=值[:值] ③ 值用来描述进程环境信息。
存储形式:与命令行参数类似。char *[]
数组,数组名environ,内部存储字符串,NULL作为哨兵结尾。
使用形式:与命令行参数类似。
加载位置:与命令行参数类似。位于用户区,高于stack的起始位置。
引入环境变量表:extern char ** environ;
例子:打印当前进程的所有环境变量。
#include <stdio.h>
#include <unistd.h>
extern char **environ;
int main(void)
{
int i;
for (i = 0; environ[i] != NULL; i++)
printf("%s\n", environ[i]);
return 0;
}
例子:简单实现getenv函数。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
extern char **environ;
char *my_getenv(const char *name)
{
char *p = NULL;
int i, len;
for(i = 0; environ[i] != NULL; i++){
p = strstr(environ[i], "=");//strstr(str1,str2) 函数用于判断字符串str2是否是str1的子串。如果是,则该函数返回str2在str1中首次出现的地址;否则,返回NULL。
len = p - environ[i];
if(strncmp(name, environ[i], len) == 0){
return p+1;
}
}
return NULL;
}
int main(int argc, char *argv[])
{
char *p = NULL;
//p = getenv(argv[1]);
p = my_getenv(argv[1]);
if(p == NULL)
printf("there is no match\n");
else
printf("%s\n", p);
return 0;
}
常见环境变量
按照惯例,环境变量字符串都是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。
TERM
当前终端类型,在图形界面终端下它的值通常是xterm,终端类型决定了一些程序的输出显示方式,比如图形界面终端可以显示汉字,而字符终端一般不行。
LANG
语言和locale,决定了字符编码以及时间、货币等信息的显示格式。
HOME
当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。
getenv函数
获取环境变量值
char *getenv(const char *name);
成功:返回环境变量的值;失败:NULL (name不存在)
setenv函数
设置环境变量的值
int setenv(const char *name, const char *value, int overwrite);
成功:0;失败:-1
参数overwrite取值:
1:覆盖原环境变量
0:不覆盖。(该参数常用于设置新环境变量,如:ABC = haha-day-night)
unsetenv函数
删除环境变量name的定义
int unsetenv(const char *name); 成功:0;失败:-1
注意事项:name不存在仍返回0(成功),当name命名为"ABC="时则会出错。(“=”不能有)
进程状态
进程基本的状态有5种。分别为创建态,就绪态,运行态,阻塞态与结束态。
getpid函数
获取当前进程ID
pid_t getpid(void);
getppid函数
获取当前进程的父进程ID
pid_t getppid(void);
fork函数
创建一个子进程。
pid_t fork(void);
失败返回-1;成功返回:① 父进程返回子进程的ID(非负) ②子进程返回 0
注意返回值,不是fork函数能返回两个值,而是fork后,fork函数变为两个,父子需各自返回一个。
父进程的fork返回一个非负ID,子进程的fork返回0
例1:使用fork创建单个子进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
pid_t pid;
printf("xxxxxxxxxxxxxx\n");
pid=fork();
if(pid==-1)
{
perror("fork error");
exit(1);
}else if(pid==0)
{
printf("I am child,pid=%u,ppid=%u",getpid(),getppid());
}
else
{
sleep(1);//让子进程先结束
printf("I am father,pid=%u,ppid=%u",getpid(),getppid());
}
printf("yyyyyyyyyyyyyyyy\n");
return 0;
}
例2:使用fork循环创建N个子进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
int i;
pid_t pid;
printf("xxxxxxxxxxxxxx\n");
for(i=0;i<5;i++)
{
pid=fork();
if(pid==-1)
{
perror("fork error");
exit(1);
}
else if(pid==0)
{
break;
}
else
{
//父进程do somthing
}
}
if(i<5)
{
sleep(i);//这样是为了子进程顺序好看
printf("I am %d child,pid=%u\n",i+1,getpid());
}
else
{
sleep(i);
printf("I am parent\n");
}
return 0;
}
进程共享
父子进程之间在fork后。有哪些相同,那些相异之处呢?
刚fork之后:
父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…
父子不同处: 1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集
似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?
当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
核心点:父子进程共享:1. 文件描述符(打开文件的结构体) 2. mmap建立的映射区 (进程间通信详解)
例子:父子进程对全局变量的操作
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int var=50;
int main()
{
pid_t pid;
printf("xxxxxxxxxxxxxx\n");
pid=fork();
if(pid==-1)
{
perror("fork error");
exit(1);
}
else if(pid>0)
{
//父进程
sleep(5);//让子进程先执行完
printf("I am father pid=%d\n",getpid());
}
else
{
var=100;
printf("I am child pid=%d\n",getpid());
}
printf("var=%d\n",var);
return 0;
}
可见,由于上面这个例子的全局变量发生了写操作,所以这个例子中的全局变量父子进程是不共享的!
总结一下:不改就共享,改就独享。
gdb调试
使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过指令设置gdb调试工具跟踪父进程或者是跟踪子进程。默认跟踪父进程。
set follow-fork-mode child 命令设置gdb在fork之后跟踪子进程。
set follow-fork-mode parent设置跟踪父进程。
注意,一定要在fork函数调用之前设置才有效。