程序是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程。
二进制格式标识:每个程序文件都包含用于描述可执行文件格式的元信息。内核利用此信息来解释文件中的其他信息。(ELF可执行连接格式)
机器语言指令:对程序算法的编码
程序入口地址:标识程序开始执行时的起始指令在内存中的位置
数据:程序文件包含的变量初始值和程序使用的字面量值(比如字符串””)
符号表以及重定位表:描述程序中函数和变量的位置及名称。这些表格有多重用途其中包括 调试和运行时的符号解析(动态链接)。
共享库和动态链接信息:程序文件所包含的一些字段,列出了程序运行时需要的的共享库以及加载共享库的动态连接器的路径名。
其他信息:….用于描述如何创建进程
进程是正在运行的程序的实例。是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。他是操作系统动态执行的基本单元,在传统的操作系统中也是分配资源的基本单元。可以用一个程序来创建多个进程,进程是内核定义的抽象实体,并为该实体分配用以执行程序的各项系统资源。从内核的角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码及代码所使用的变量,而内核数据结构则用于维护进程状态信息。记录在内核数据结构中的信息包括许多与进程相关的标识号(IDS)、虚拟内存表、打开文件的描述符表(fd)、信号传递及处理的有关信息、进程资源使用及限制、当前工作目录和大量的其他信息。
单道程序,即在计算机内存中只允许一个程序运行。多道程序设计是在计算机内存中同时存放多道相互独立的程序,使他们在os管理下相互穿插并发地执行。两个或两个以上程序在计算机系统中同处于开始到结束之间的状态,这些程序共享计算机系统资源(注意不共享线程独有的一些资源、共享的是一些独立的临界资源),这样可以大大地提高cpu利用率。
对于一个单cpu系统来说,多进程并发执行,但就微观而言任意时刻cpu上运行的程序只有一个。多个进程轮流使用cpu,当下的常见cpu为纳秒级,1秒可以执行大约10亿条指令。由于人眼的反应是毫秒级,所以看似同时在运行。
时间片又称”量子”或”处理片”是操作系统分配给每个正在运行的进程微观上的一段cpu时间,在Linux上为5ms-800ms。时间片由操作系统内核的调度程序分配给每个进程。首先,内核会给每个进程分配相等的初始时间片。然后每个进程轮番地执行相应的时间,当所有进程都处于时间片耗尽的状态时,内核会重新为每个进程计算并分配时间片如此往复。
并行(parallel):指在同一时刻,有多条指令在多个cpu上同时执行。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上有”同时执行”的效果。在微观上并不是同时执行的只是在若干时间内,多个进程快速交替执行。
![]() |
|---|
进程控制块,os内核为每个进程分配一个PCB(processing control block)进程控制块,维护进程相关的信息,Linux的进程控制块是task_struct结构体。在/usr/src/linux-headers-xxx/include/linux/sched.h文件中可以查看task_struct结构体定义,主要掌握以下部分
进程id:系统中每个进程唯一的id用pid_t类型表示,是一个非负整数
进程的状态:有就绪、运行、挂起、停止等状态
进程切换时需要保存和恢复的一些cpu寄存器
描述虚拟地址空间的信息
描述控制终端的信息
当前的工作目录
umask掩码
文件描述符表fd表,包含很多指向文件结构体的FILE指针
和信号相关的信息
用户id和组id
会话(session)和组id
进程可以使用的资源上限 ulimt -a
*2进程状态转换
进程状态反应进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中进程分为三个剧本状态,就绪态 运行态 阻塞态。简单讲下状态的转化(具体的去看王道操作系统的笔记)。
运行态: 进程当前占有cpu 进程正在cpu上跑
就绪态:进程具备运行条件(需要的资源已经全部到位 就差cpu)。就绪态的进程编号存在就绪队列中,根据调度算法选出一个放在cpu上跑
阻塞态:又称为等待态或睡眠态,指进程不具备运行条件(除了cpu外还差其他资源),正在等待某个事件的完成
![]() |
|---|
![]() |
|---|
进程进入终止态的进程以后不再执行,但依然保留在os系统中等待善后(比如子进程结束后等待父进程回收其使用的资源后再删除)
进程相关命令
ps aux a:显示终端上的所有进程 包括其他用户的进程(可有多用户登录同一服务器) u:显示进程的详细信息 x:显示没有控制终端的进程
root(USER用户名) 1(PID进程ID) 0.1(%CPU这个进程占用了多少cpu) 0.2(%MEM这个进程占用了多少内存) 225420(VSZ ) 9104(RSS) pts/1(TTY 当前进程所属于的终端) Ss(STAT 状态) 19:02(START 线程启动时间) 0:02(TIME 线程已经运行时间) -bash(COMMAND 什么命令启动了这个线程)
tty 命令查看当前界面属于哪个终端
STAT 状态参数意义 表示这个进程是:
D 不可终端uninterruptible (usually IO)
R 正在运行 或在队列中的进程
S(大写) 处于休眠状态
T 停止或被追踪
Z 僵尸进程
W 进入内存交换(从内核2.6开始无效)
X 死掉的进程
< 高优先级
N 低优先级
s 包含子进程
+ 位于前台的进程组
….还要一些常见的这里没列出来
ps ajx j:列出与作业控制相关的信息
PPID(这个进程的父进程ID) PID(这个进程的ID) PGID(进程组的ID) SID(会话ID ) 多个进程组成一个进程组 多个进程组组成一个会话 TTY TPGID STAT UID TIME COMMAND
上面的两个命令为进程快照 只是列出在那个时刻的所有进程 而非实时的更新
实时显示进程动态状态
top 可以 top -d 刷新时间 来指定信息更新的时间间隔(单位为秒) 在top执行后 可以按以下按键来对显示结果排序(都是降序排序)
M 根据内存使用量排序
P 根据CPU占有率排序
T 根据进程运行时间长短来排序
U 根据用户名来筛选进程 (按u后会让你输入进程名)
K 输入指定的PID来杀死那个对应进程(按k后会让你输入 PID号)
杀死进程
kill [-signal 可选参数] pid
kill -l 列出所有信号
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
kill -SIGKILL pid 根据上面的序号表我们知道 SIGKILL就是9
kill -9 pid 加 -9 表示的是 9 号信号。其实kill并不是杀死进程,而是发送信号,而 9 号信号能够强制杀死进程,其它的信号有的可以有的不可以
killall name 根据进程名杀死进程
这里插一句 c标准库中 sleep(1);这里的1表示的是1秒 usleep(1);这里的1表示的是1微秒
再插一句 在执行可执行文件时可以在后面加个&号 表示在后台执行 不会占用我们当前的终端,当前终端还可以执行其他命令 并且返回这个可执行文件启动的进程的进程号。但是printf的信息依旧会在当前窗口打印 但是不影响。 ./a.out &
进程号和相关函数
每个进程都由进程号来标识,其类型为pid_t(是整型),进程号范围 0-32767。进程号总是唯一的,但可以重用(比如当一个进程终止后 其进程号其他进程就可以再次使用)。
任何进程(除init进程)都是由另一个进程创建,该进程称为被创建的父进程,对应的进程号称为父进程号(PPID)。
进程组是一个或多个进程的集合,他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号,默认情况下,当前的进程号会当作当前的进程组号。
进程号和进程号相关函数
pid_t getpid(void); //获取当前进程的进程号
pid_t getppid(void);//得到当前进程的父进程函数
pid_t getpgid(pid_t pid);//得到进程的进程组号 传null默认返回当前进程所在组的id
3进程创建
子进程也是进程 是OS分配资源的单位
系统允许一个进程创建新进程,新进程即这个进程的子进程,子进程还可以创建新的子进程,形成进程树结构模型
/*#include <sys/types.h>#include <unistd.h>pid_t fork(void); //系统调用fork的返回 会返回两次 一次在调用fork的这个父进程中返回子进程的id 一次在子进程中返回0通过fork的返回值 来区分子进程和父进程成功 子进程中返回0 父进程中返回子进程的pid失败返回-1(只在父进程中反应 因为子进程创建失败了所以不会有返回) 失败的两个主要原因 1当前系统的进程数已经达到了系统规定的上限此时errno的值被设置为EAGAIN2系统内存不足,此时errno的值设置为ENOMEMfork() creates a new process by duplicating the calling process.fork通过复制当前进程的一些信息来实现创建子进程。The child process and the parent process run in separate memory spaces.(子进程和父进程在不同的内存空间运行)At the time of fork() both memory spaces have the same content.(在fork时 两块空间有相同的内容) Memory writes, file mappings (mmap(2)), and unmappings (munmap(2)) performed by one of the processes do not affect the other.在对内存进行写时file mappings能保证写操作只针对那个进程而不影响其他进程。*/#include <sys/types.h>#include <unistd.h>#include <stdio.h>int main(){//创建子进程pid_t pid = fork();//fork后 子进程和父进程都会继续向下执行这些相同的语句!!!!!// 相当于在fork后出现了两个分支 并且两个分支同时向下交替运行(并发)!!!!!//通过fork的id判断当前为父进程还是子进程if (pid > 0) //>0 则说明fork是在父进程中返回子进程的id号{printf("child pid:%d", pid);//当前是父进程printf("parent:pid:%d ppid:%d\n", getpid(), getppid());}else if (pid == 0) //==0 说明fork在子进程中返回0{//当前是子进程printf("child:pid:%d ppid:%d\n", getpid(), getppid());}for (int i = 0; i < 3; i++){printf("pid:%d i:%d", getpid(), i);sleep(1);}//child pid:87651//parent:pid:87650 ppid:87186(终端进程)//pid:87650 i:0 父进程//child:pid:87651 ppid:87650//pid:87651 i:0 子进程//pid:87651 i:1 子进程//pid:87650 i:1 父进程//pid:87651 i:2 子进程//pid:87650 i:2 父进程//交替运行的顺序随机 由cpu状态和调度算法决定//并且由i值可以看出 父进程与子进程的变量是两套变量不是共有的 从侧面印证了父子进程的内存空间不在一块return 0;}
4父子进程虚拟地址空间情况
pid_t pid = fork();
父进程会执行fork,并得到返回值
子进程不会执行fork,而是直接得到返回值
框内分别为父进程和子进程会执行的代码
![]() |
|---|
这里如果打印父子进程中变量的地址可以发现地址全部相同,但其实打印出来的地址是虚拟地址空间而非真实的单个进程的虚拟地址空间
![]() |
|---|
调用fork后的虚拟地址空间
fork后的子进程的用户数据和父进程的一样,这一点在man 2 fork的第一行就提到了(注意是数据相同 而非空间相同)
fork在创建子进程时,会拷贝父进程的信息。但是当子进程被创建出来后,fork结束开始返回的时候,两个进程是独立的互不影响的,在父进程返回子进程的pid,在子进程返回0。子进程被创建出来后,子进程不会从头开始执行代码而是根据从父进程拷贝来的代码运行指针得知当前运行到哪了,再从那个地方开始执行。
fork后的子进程的内核区的数据也会相同但是pid不同用以区分父子进程
![]() ![]() |
|---|
实际上,更准确来说,linux的fork()使用是通过写时拷贝(copy on write)实现。写时拷贝是一种可以推迟甚至避免拷贝数据的计数。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个(物理)地址空间。只有在需要写入的时候才会复制地址空间,从而使各个进程拥有各自的地址空间。也就是说,资源的复制只在写入(创建变量,变量赋值,调用函数。。。)的时候才会进行,在此之前只有以只读方式共享。
注意:fork之后父子进程共享已经打开的文件,fork产生的子进程与父进程拥有相同的文件描述符指向相同的文件描述符表,拥有相同的引用计数,相同的共享文件偏移指针。
父进程在fork前打开的所有文件描述符都在子进程中保存了(每个进程都有独立的描述符表)。由于所有的进程共享文件表、v-node表,所以父子进程的描述符表也是相同的,所以父子进程对文件是以共享的方式存在的。 由于父子进程是以共享的方式控制已经打开文件的,因此对文件的操作也是相互影响的,因此读写文件的位置也会发生相应的改变。父(子)进程的文件读写位置会随着子(父)进程的文件读写位置改变而改变,因为此时改变的是文件表的文件位置项,而文件表是所有进程共享的,任何一个进程的修改都会影响到别的进程。但是父(子)进程对描述符的修改不会影响子(父)进程的描述符,比如close(fd)的操作只是改变文件表述符表中的内容,而该表是每个进程相互独立的,因此不会改变其他进程的表。
父子进程的文件描述符可以共享,但是要在fork之前打开,并在公有代码中关闭文件(因为文件描述符表是独立的 在公有代码中关闭 会按并发的顺序依次关闭)。
文件描述符表每个进程都有一个存在内核区的pcb里
虚拟地址空间补充
虚拟地址是程序运行时,程序访问存储器所使用的逻辑地址称为虚拟地址,通过逻辑地址映射到真正的物理内存上。
1.进程虚拟地址空间,也就是意味着认为规定的逻辑地址空间。
2.每个进程都可拥有3G的虚拟地址空间(线性地址空间可达4G(2^32)linux的虚拟地址空间 一般3G分给用户区 1G分给内核区),并且用户进程之间的地址是互不可见,互不影响的,也就是说,即使俩个进程对同一块地址进行操作,也不会产生问题。
3.虚拟地址是不具备存储能力的,数据的存储依然要存放在物理内存中。
4.可以通过页表映射从逻辑地址空间访问真实的物理地址空间。
![]() |
|---|
写时拷贝:当父进程定义了一个全局变量,fork出来一个子进程,当子进程发生对该全局变量发生修改的时候,操作系统会重新开辟一段空间来保存子进程更改后的值,则子进程会更改自己的页表映射关系。
如果说父子进程都不改变原来的内存当中的值,页表的映射关系还是映射到同一块物理内存当中。
1.当创建一个子进程的时候,子进程会拷贝父进程的页表结构,也就是说会将父进程虚拟地址空间和物理内存之间的映射关系也拷贝了。2.如果创建完成子进程,子进程也会通过页表结构映射到物理内存的同一块区域。3.如果父子进程都不进行修改,则映射的关系不会修改。
![]() |
|---|
子进程页表绿色虚线为子进程aa改变前对应的物理内存,子进程页表绿色实线为aa改变后对应的物理内存。这里注意父子进程同一变量的虚拟地址空间是相同的。
实际上,fork后子进程和父进程共享的资源还包括:
打开的文件
实际用户ID、实际组ID、有效用户ID、有效组ID
添加组ID
进程组ID
会话期ID
控制终端
设置-用户-ID标志和设置-组-ID标志
当前工作目录
根目录
文件方式创建屏蔽字
信号屏蔽和排列
对任一打开文件描述符的在执行时关闭标志
环境
连接的共享存储段(共享内存)
资源限制
父子进程之间的区别是:
fork的返回值
进程ID
不同的父进程ID
子进程的tms_utime,tms_stime,tms-cutime以及tms_ustime设置为0
父进程设置的锁,子进程不继承
子进程的未决告警被清除
子进程的未决信号集设置为空集









