目录
1 进程
- 进程:程序的一个执行实例,正在执行的程序等
1.1 进程PCB
- 进程 = 该进程内核数据结构(PCB) + 该进程对应的磁盘上的可执行程序
Linux 操作系统下的 PCB 是 task_struct,装载到内存里并且包含着进程的信息。
- PCB内部常见属性
标示符: 描述本进程的唯一标示符,用来区别其他进程
状态: 任务状态,退出代码,退出信号等
优先级: 相对于其他进程的优先级
程序计数器: 程序中即将被执行的下一条指令的地址
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据
I/O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
其他信息
- 查看进程的方式
- ps
- /proc是Linux系统下以文件形式查看进程的目录
1.2 fork创建子进程
- 新增一个进程,分配新的内核数据结构和内存块给子进程,将父进程部分数据结构内容拷贝给子进程,将子进程添加到系统进程列表,fork返回,调度器开始调度
- 子进程拥有独立的PCB,创建成功后,独立运行自己的代码
- fork函数两个返回值
创建子进程的目的是为了让子进程执行其他任务,子进程返回0,父进程返回子进程的pid,因此通过不同的返回值,确定彼此是父进程还是子进程。
父进程能创建多个子进程,为保证父进程能控制多个子进程,父进程的返回值是子进程的pid,而对子进程而言,父进程唯一,因此子进程返回0.
举例如下
#include<stdio.h>
#include<unistd.h>
int main(){
pid_t id = fork();
if(id < 0){
// 创建失败
perror("fork");
return 1;
}
else if(id == 0){
//子进程
while(1){
printf("I an child, pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
else{
//父进程
while(1){
printf("I an parent, pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
sleep(1);
return 0;
}
运行结果如下
1.3 进程状态
linux源码里定义的进程状态如下:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
- 运行状态R:运行状态不代表该进程一定占用CPU,而是在运行队列中等待或者正在占用CPU,叫做运行态或者就绪态
- 浅睡眠状态S:又称可中断睡眠,该状态的进程等待非CPU的资源,一旦等到需求的资源或者接受到信号,便会转变为运行状态
- 挂起状态:也属S状态,发生在内存不足时,操作系统将部分进程的代码和数据转移到磁盘中,而PCB不换,被转移的进程的状态称为挂起状态
- 深度睡眠状态D:又称不可中断睡眠,处于该状态的进程不可被杀掉,只有等其自动醒来
- 暂停状态T:通过SIGSTOP信号暂停进程,可通过SIGCONT信号让进程继续运行
- 死亡状态X:终止状态,具有很强的瞬时性,很难在任务列表中查看到
- 僵尸状态Z:子进程退出,父进程没有读取子进程退出时的退出码,子进程就会进入僵尸状态
1.4 僵尸进程
- 子进程退出后,父进程未读取子进程状态,子进程进行僵尸状态,子进程会一直存留在进程表中,一直等待父进程读取
- 僵尸进程会占用内存空间,长时间占用会导致内存泄漏
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
// 测试僵尸进程
int main(){
pid_t id = fork();
if(id < 0){
// 创建失败
perror("fork");
return 1;
}
else if(id == 0){
//child process
while(1){
printf("I an child, pid:%d, ppid:%d\n", getpid(), getppid());
sleep(3);
break;
}
exit(0);
}
else{
//parent process
while(1){
printf("I an parent, pid:%d, ppid:%d\n", getpid(), getppid());
sleep(3);
}
}
return 0;
}
监听命令
while :; do ps axj | head -1 && ps axj | grep myproc | grep -v grep; sleep 1; echo "########################"; done
子进程运行三秒后退出,父进程不读取子进程状态,子进程进入僵尸状态
如下图所示
1.5 孤儿进程
- 父进程先于子进程退出,子进程变成孤儿进程,孤儿进程会被1号进程领养
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
pid_t id = fork();
if(id < 0){
// 创建失败
perror("fork");
return 1;
}
else if(id == 0){
//child process
while(1){
printf("I an child\n");
sleep(1);
}
}
else{
//parent process
int count = 5;
while(count){
printf("I an parent %d\n", count--);
sleep(1);
}
}
return 0;
}
父进程运行5秒后退出,子进程变成孤儿进程,其ppid由父进程pid更改为1;此时无法使用
ctrl+c
停止进程,只能使用kill -9杀掉子进程
如图所示
1.6 进程优先级
-
进程优先级是进程分配CPU资源的先后顺序;因为CPU资源有限,通过优先级方式,让较为重要的进程优先占用CPU
-
查看优先级
ps -l
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值,优先值的修正数据;调整进程优先级,在Linux下,就是调整进程nice值
UID : 代表执行者的身份
- 调整进程优先级
top; 进入top后按"r" → 输入进程PID → 输入nice值
将进程nice值设为1后,进程优先级变为81
nice值的取值范围是-20到19。一共40个级别
2 环境变量
2.1 环境变量的定义
- 环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数;其本质是操作系统在内存/磁盘文件中开辟的空间,用来保存系统相关的数据
我们在Linux系统里输入
ls
便能查看当前路径下的文件名,而要想执行在当前路径下我们编译的可执行文件,却需要添加./
。
which ls
可以发现ls存放的路径在/usr/bin/ls
echo $PATH
查看环境变量,可发现环境变量中保存了/usr/bin
我们执行系统命令ls
,系统会先在环境变量中查找,这就是环境变量发生作用,因此在我们执行系统命令时可以不带路径:
我们想不带路径就执行我们自己的可执行文件,有两种途径
- 将我们的可执行文件拷贝到环境变量PATH的某个路径下(不推荐,因为会污染命令池)
- 将存放我们的可执行文件的路径添加到环境变量PATH中
通过
export PATH=$PATH:当前路径
将路径添加到环境变量中;要注意的是,本次修改只对本次登录有效,在不修改配置文件的前提下,不会永久保存
将路径添加到环境变量前,执行
mytest
会报错,通过export PATH=$PATH:/home/yjp/linux-c
添加环境变量后,便能成功执行mytest
2.2 常见的环境变量及指令
- 环境变量:
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中默认的目录)
SHELL : 当前Shell,它的值通常是/bin/bash。 - 指令:
echo $NAME
能显示NAME环境变量
export
能设置一个环境变量
env
能显示所有环境变量
set
显示本地定义的shell变量和环境变量
unset
清楚环境变量
2.3 查看环境变量
- 通过命令行第三个参数
#include<stdio.h>
int main(int argc,char* argv[],char* env[]) {
for(int i = 0; env[i]; i++) {
printf("%d->%s\n",i,env[i]);
}
return 0;
}
- 通过第三方变量
environ
获取
通过man environ
查看,系统提供了一个全局变量environ
指向环境表的起始地址,本质上与第一种方法一致
#include <stdio.h>
int main(int argc, char *argv[]){
extern char **environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
getenv()
通过传环境变量名NAME获取环境变量的内容(此方法最常用)
#include <stdio.h>
#include <stdlib.h>
int main(){
printf("%s\n", getenv("PATH"));
printf("%s\n", getenv("HOME"));
return 0;
}
2.4 环境变量的全局属性
- 环境变量的全局实现能够实现被子进程继承
#include<stdio.h>
#include<stdlib.h>
int main(){
printf("%s\n",getenv("myenv"));
return 0;
}
通过
export myenv="myenvtest"
,将我们定义的环境变量添加到系统的环境变量中,./getmyenv
的本质是我们的父进程bash创建子进程执行我们的程序,我们的程序作为子进程,继承父进程的环境变量,打印出我们把保存在系统里的环境变量myenv
3 进程地址空间(难点)
3.1 地址空间分布
在Liunx环境下,运行如下代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[]){
const char *str = "helloworld";
static int test = 10;
printf("test static addr: %p\n", &test);
printf("read only string addr: %p\n", str);
printf("code addr: %p\n", main);
printf("init global addr: %p\n", &g_val);
printf("uninit global addr: %p\n", &g_unval);
char *heap_mem = (char*)malloc(10);
char *heap_mem2 = (char*)malloc(10);
char *heap_mem3 = (char*)malloc(10);
char *heap_mem4 = (char*)malloc(10);
printf("heap addr: %p\n", heap_mem);
printf("heap addr: %p\n", heap_mem2);
printf("heap addr: %p\n", heap_mem3);
printf("heap addr: %p\n", heap_mem4);
printf("stack addr: %p\n", &heap_mem);
printf("stack addr: %p\n", &heap_mem2);
printf("stack addr: %p\n", &heap_mem3);
printf("stack addr: %p\n", &heap_mem4);
int i;
for(i = 0 ;i < argc; i++) {
printf("argv[%d]: %p\n", i, argv[i]);
}
for(i = 0; env[i]; i++) {
printf("env[%d]: %p\n", i, env[i]);
}
return 0;
}
code addr
代码区的地址最小;
read only string addr
只读字符串开辟在代码区;
init global addr
初始化全局变量,比uninit global addr
未初始化的全局变量地址小;
test static addr
静态数据开辟在全局变量区;
heap addr
堆地址向上开辟;stack addr
栈地址向下开辟;堆地址比栈地址小,堆栈地址相向开辟
3.2 进程地址空间
- 进程地址空间是什么
- 进程地址空间是操作系统给每个进程分配的空间,其本质上是
虚拟地址空间
,为了保证安全性,用户无法直接访问物理地址空间
#include <stdio.h>
#include <unistd.h>
int g_val = 100;
int main(){
pid_t pid = fork();
if( pid == 0 ){
int cnt = 0;
while(1){
printf("I an child, pid: %d, ppid= %d, g_val= %d, &g_val= %p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
cnt++;
if( cnt == 5 ){
g_val = 200;
printf("child change g_val 100 -> 200 success\n");
}
}
}
else{
while(1){
printf("I an father, pid: %d, ppid= %d, g_val= %d, &g_val= %p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
}
}
return 0;
}
父子进程使用同一个全局变量,地址相同,值可不同,结论:全局变量的地址不是物理内存的地址,其地址是虚拟地址(线性地址)
- 进程地址空间怎么设计
- 进程地址空间是一种
内核数据结构
,里面存放了各种数据存放区域
的起始地址和终止地址
,也就是上文介绍的地址空间分布 - 通过
页表和存储管理单元
将虚拟地址空间和物理地址空间建立映射关系。
通过页表,即时上文的父子进程对于
同一个全局变量
具有相同的虚拟地址
,但是通过页表
的不同映射关系,同一个全局变量映射到了物理地址空间上的不同地址
,因此同一个全局变量具有相同的地址
,却有不同值
,保证了进程的独立性
- 为什么需要进程地址空间
- 保护物理地址空间
- 提高内存空间利用率
通过虚拟地址,屏蔽操作系统申请物理内存的过程,只有用户需要对真正的物理内存进程访问时,操作系统才申请相关的内存,构建映射关系
- 保证进程独立性
通过虚拟地址空间,让用户进程的数据有序,便于管理;通过映射,让不同进程拥有独立的物理地址空间,实现用户进程独立性
3.3 挂起状态的理解
- 进程包括内核数据结构和代码数据两部分组成;创建进程时,操作系统不是在第一时间为进程开辟物理地址空间,在极端情况下,只有进程的内核数据结构被创建出来;只有当这个进程要被执行时,操作系统才为该进程创建对于的映射关系
- 当该进程短时间内不再执行时,操作系统便将该进程的代码和数据
从内存中取出
,该进程的状态变为挂起状态
;此时,页表的映射关系,从内存转移到磁盘