Linux系统编程-进程
进程的概念
- 程序和进程
- 程序,是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁….)
- 进程,是一个抽象的概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资源。在内存中执行。(程序运行起来,产生一个进程)
- 进程所有的数据都在内存
- 需要占用系统资源(cpu、物理内存、硬盘(虚拟地址空间,跟磁盘上占用的空间不一样))
- 举例:
程序 → 剧本(纸) 进程 → 戏(舞台、演员、灯光、道具...)
- 同一个剧本可以在多个舞台同时上演。同样,同一个程序也可以加载为不同的进程(彼此之间互不影响)
- 如:同时开两个终端。各自都有一个bash但彼此ID不同。
- 并发和并行
- 并发,在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但,任一个时刻点上仍只有一个进程在运行。
- 例如,当下,我们使用计算机时可以边听音乐边聊天边上网。 若笼统的将他们均看做一个进程的话,为什么可以同时运行呢,因为并发。
- 通俗举例:
- 并发:
- 不是一个时间点的概念,是一个时间段的概念
- 一个咖啡机,200个人用杯子接,1分钟之内都要喝上,那么就每人接一口,然后循环。
- 并发量指的就是一段时间内完成了多少任务。
- 并行:
- n个咖啡机,同时工作,200个人接。
- 并发:
- 单道程序设计
- 所有进程一个一个排对执行。若A阻塞,B只能等待,即使CPU处于空闲状态。而在人机交互时阻塞的出现时必然的。所有这种模型在系统资源利用上及其不合理,在计算机发展历史上存在不久,大部分便被淘汰了。
- 多道程序设计
- 在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行。多道程序设计必须有硬件基础作为保证。
- 时钟中断即为多道程序设计模型的理论基础。 并发时,任意进程在执行期间都不希望放弃cpu。因此系统需要一种强制让进程让出cpu资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。 操作系统中的中断处理函数,来负责调度程序执行。
- 在多道程序设计模型中,多个进程轮流使用CPU (分时复用CPU资源)。而当下常见CPU为纳秒级,1秒可以执行大约10亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。
- 实质上,并发是宏观并行,微观串行!
- CPU和MMU
-
CPU运行原理:
- 先把磁盘数据缓存到硬盘中,内存相对于硬盘存储容量变小,但是速度变快了
- 然后把内存的数据放到缓冲区中(cache),然后在通过cache导入到寄存器中。
- 寄存器再交给cup进行运算
- 程序的运行过程
- 将a.out从网络下载到磁盘中
- 当运行a.out时,产生进程,然后程序加载到内存中
- 然后通过内存加载到缓冲区cache
- 然后通过cache进入到CPU
- 但是实际上进入到CPU的并不是程序的全部,而是一条将要执行的一条指令
- cup每次从缓冲区取一条指令,由CPU中的预取器来完成
- 然后预取器将这条指令交给译码器(add eax,ebx)
- 译码器分析这条指令作用,需要哪些寄存器,然后将相应的数据存入寄存器
- 然后译码器交给ALU算术单元进行运算(ALU只会加法与左移运算)
- 将运算结果在写入到寄存器中
- 然后由寄存器再返回给缓存cache
- 直到物理设备显示。
-
MMU的基本工作原理
- MMU位于CPU内部的硬件,叫内存管理单元
- MMU的作用:完成虚拟内存与物理内存的映射
- 程序运行先加载到虚拟内存,这个虚拟内存地址是0-4g
- 也就是说你这个程序运行有0-4g的内存地址可以分配,这个地址是虚拟的,假设分配的,即预设的
- 由于这个程序的所有数据分配的是虚拟内存地址,但是最终还是要加载到实际的物理内存中
- 那么这就需要将虚拟内存地址转化为真实的物理内存地址。
- 这个转化功能就是MMU来完成的
- 我们程序员只需要关注数据的虚拟内存地址即可。
- MMU的作用:设置修改内存访问级别
- 对于虚拟内存来说,0-3g为用户区,3-4g为内核区,但是物理内存是没有做区分的
- 此时,MMU在将虚拟内存映射到物理内存的同时,设置改内存的访问级别,这个访问级别是给CPU设置的
- 正常情况下interl的CPU有4种访问级别(3、2、1、0,从小到大)
- 比如printf的调用,先访问用户区(3)
- 然后内部实际调用的是内核方法,然后访问的是内核区(0),调用硬件显示数据。
- 什么是内核?
- 操作系统的核心程序,简称内核
- 操纵计算机硬件工作,辅助所有的进程运行的。
- 由于同一个操作系统下内核是一样的,所以多个进程之间虚拟内存映射到物理内存中的内核是同一块内存。
- MMU什么时候被使用到呢?
- 预取器通过虚拟内存地址取数据时,通过MMU映射成物理内存地址,然后取出数据
- 同理计算结果从寄存器写入到内存时,写入的也是虚拟内存地址,然后通过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寄存器。
- n个进程使用同一个cpu与寄存器,当cpu去处理其他进程时,所以要暂时保存
- 描述虚拟地址空间的信息。
- MMU处理完之后会生成一张虚拟地址与物理地址的对应表存储到pcb中
- 描述控制终端的信息。
- 当前工作目录(Current Working Directory)。(当前进程的工作目录)
- umask掩码。
- 文件描述符表,包含很多指向file结构体的指针。
- 和信号相关的信息。
- 用户id和组id。
- 会话(Session)和进程组。
- 进程可以使用的资源上限(Resource Limit)。
- 进程状态
- 进程基本的状态有5种。分别为初始态,就绪态,运行态,挂起态与终止态。其中初始态为进程准备阶段,常与就绪态结合来看。
- 就绪:等待cpu分配时间片
- 运行: 占用cpu
- 挂起:等待除cpu以外的其他资源,主动放弃cpu
- 停止
- 进程基本的状态有5种。分别为初始态,就绪态,运行态,挂起态与终止态。其中初始态为进程准备阶段,常与就绪态结合来看。
环境变量
- 一个操作系统,n个用户登录,每个用户的使用习惯不一样,每个用户设置各自的使用习惯,这个设置的对象就是环境变量。同样,如果以进程为单位,每个进程在操作系统中的环境变量也可以自己设置。
- 环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。通常具备以下特征:
- 字符串(本质)
- 有统一的格式:名=值[:值]
- 值用来描述进程环境信息。
- 存储形式:与命令行参数类似。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
。
- 当前Shell,它的值通常是
- 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="时则会出错
-
-
代码举例
#include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { char *val; const char *name = "ABD"; val = getenv(name); printf("1, %s = %s\n", name, val); setenv(name, "haha-day-and-night", 1); val = getenv(name); printf("2, %s = %s\n", name, val); #if 1 int ret = unsetenv("ABD="); printf("ret = %d\n", ret); val = getenv(name); printf("3, %s = %s\n", name, val); #else int ret = unsetenv("ABD"); //name=value:value printf("ret = %d\n", ret); val = getenv(name); printf("3, %s = %s\n", name, val); #endif return 0; }
进程控制
如何在某个文件目录下创建一个进程呢?除了执行一个可执行程序自动创建一个进程之外,还有其他办法创建吗? 也可以通过函数fork来创建
fork函数
-
通过man fork查看
NAME //创建一个子进程 fork - create a child process SYNOPSIS #include <sys/types.h> #include <unistd.h> pid_t fork(void);
- 调用fork创建的进程被称为当前进程的子进程。
- 返回值有2个:
- 返回子进程的pid(非负整数)
- 返回0
- 注意返回值,不是fork函数能返回两个值,而是fork后,fork函数变为两个,父子需【各自】返回一个。
- 当前进程执行代码,执行到fork函数时创建一个子进程,这个子进程的代码跟原来进程(父进程)的代码一样,但是并不会重头开始执行,而是从fork开始往后执行
- 那么这样一来,就有2个进程,2套同样的代码,一起执行,原来的进程被称作父进程。
- 那么也就意味着有2个fork函数,父进程一个,子进程一个
- 父进程fork返回的返回值是子进程的id
- 子进程fork返回的值是0,代表当前进程创建进程成功。
- 从而也就可以从代码中fork的返回值来判断当前进程是否是子进程。
- 如果返回-1,说明出错了
-
代码举例
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/types.h> int main(void) { pid_t pid; printf("xxxxxxxxxxxx\n"); pid = fork(); if(pid == -1) { perror("forerror"); exit(1); }else if (pid == 0){ printf("I am child progress ,pid = %u\n, ppid = %u\n",getpid(),getppid()); }else{ printf("I am parent progress ,pid = %u\n, ppid = %u\n",getpid(),getppid()); sleep(1); } printf("---------------\n"); }
-
运行
coderzhong@coderzhong-virtual-machine:~$ ./fortest xxxxxxxxxxxx I am parent progress ,pid = 110517 , ppid = 106715 I am child progress ,pid = 110518 , ppid = 110517 --------------- ---------------
-
其他函数
- getpid函数
- 获取当前进程ID:
pid_t getpid(void);
- 获取当前进程ID:
- getppid函数
- 获取当前进程的父进程ID:
pid_t getppid(void);
- 区分一个函数是“系统函数”还是“库函数”依据:
- 是否访问内核数据结构
- 是否访问外部硬件资源 二者有任一 → 系统函数;二者均无 → 库函数
- 获取当前进程的父进程ID:
- getuid函数
- 获取当前进程实际用户ID:
uid_t getuid(void);
- 获取当前进程有效用户ID:
uid_t geteuid(void);
- 获取当前进程实际用户ID:
- getgid函数
- 获取当前进程使用用户组ID :
gid_t getgid(void);
- 获取当前进程有效用户组ID:
gid_t getegid(void);
- 获取当前进程使用用户组ID :
进程共享
- 父子进程之间在fork后。有哪些相同,那些相异之处呢?
- 刚fork之后
- 父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…
- 父子不同处: 1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集
- 似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?
- 当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
- 重点注意!躲避父子进程共享全局变量的知识误区!
- 【重点】:父子进程共享:1. 文件描述符(打开文件的结构体) 2. mmap建立的映射区 (进程间通信详解)
- 特别的,fork之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法。
gdb调试
- 使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过指令设置gdb调试工具跟踪父进程或者是跟踪子进程。默认跟踪父进程。
set follow-fork-mode child
命令设置gdb在fork之后跟踪子进程。set follow-fork-mode parent
设置跟踪父进程。- 注意,一定要在fork函数调用之前设置才有效。