Linux多进程开发
进程状态转换
进程三个基本状态:(新建态),就绪态,运行态,阻塞态,(终止态)
注意:
只有就绪态和运行态可以相互转换,其它的都是单向转换。
就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;
而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。
阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,
缺少 CPU 时间会从运行态转换为就绪态。
查看进程指令
ps aux/ ajx
ps aux | grep name
a:显示终端的所有进程,包括其他用户的进程
u:显示进程的详细信息
x:显示没有控制终端的进程
j:列出与作业控制相关的信息
|:管道符,把两个应用程序连接在一起,然后把第一个应用程序的输出,作为第二个应用程序的输入。
grep :过滤,查找功能
ps :process status
进程和线程的区别
强相关(消息解码,业务处理)的处理用线程,弱相关(消息收发,消息处理)的处理用进程。
协程:协程是微线程,在子程序内部执行,可在子程序内部中断,转而执行别的子程序,在适当的时候返回接着执行。
Ⅰ 拥有资源
进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
Ⅱ 调度
线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
Ⅲ 系统开销
由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
Ⅳ 通信方面
线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。
进程间通信(IPC:Inter Processes Communication)
进程是资源分配的基本单位,不同进程(用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源
通信目的
-
数据传输:进程间数据相互传输
-
通知事件:发送消息,通知事件(如进程终止时要通知父进程
-
资源共享:多个进程共享同样的资源,需要内核提供互斥和同步进制
-
进程控制:有的进程希望完全控制另一个进程的执行(如DEBUG),此时控制进程希望能拦截另一个进程的所有陷入和异常状态并及时知道状态的改变
通信方式
管道
定义:⽤于具有亲缘关系的⽗⼦进程间或者兄弟进程之间的通信。
例如,统计一个文件中数目的指令 ls | wc -l ,管道字节流,单向
ls stdout(fd 1)管道写入端
wc stdin(fd 0)管道读取端
//调试代码小技巧
freopen("walk.in","r+","stdin");
freopen("walk.out","w+","stdout");
特点
(1)一个管道是一个字节流,从管道读取数据的进程可以读取任意大小的数据块,不管写入进程写入管道的数据块大小是多少。
(2)管道实际是一个在内核内存中维护的缓冲器,存储能力有限,不同操作系统大小不一定相同。
(3)管道读数据是一次性操作,数据一旦被读走,就在管道中被抛弃。
有名管道
匿名管道由于没有名字,只能⽤于亲缘关系的进程间通信。为了克服
这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。
有名管道以磁盘⽂件的⽅式存在,可以实现本机任意两个进程通信。
共享内存
定义*
允许多个进程可以访问同⼀块物理内存空间(段空间),不同进程可以及时看到对⽅进程中对共享
内存中数据的更新。这种⽅式需要依靠某种同步操作,如互斥锁和信号量等。
与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据
从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快。
使用步骤
◼ 调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其
他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
◼ 使用 shmat() 来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。
◼ 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,
程序需要使用由 shmat() 调用返回的 addr 值,它是一个指向进程的虚拟地址空间
中该共享内存段的起点的指针。
◼ 调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存
了。这一步是可选的,并且在进程终止时会自动完成这一步。
◼ 调用 shmctl() 来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之
后内存段才会销毁。只有一个进程需要执行这一步。
函数
◼ int shmget(key_t key, size_t size, int shmflg);
◼ void *shmat(int shmid, const void *shmaddr, int shmflg);
◼ int shmdt(const void *shmaddr);
◼ int shmctl(int shmid, int cmd, struct shmid_ds *buf);
◼ key_t ftok(const char *pathname, int proj_id);
消息队列
- 消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符 ID 来标识;
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;
- 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除;
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
信号量
- 信号量是⼀个计数器,⽤于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信⽅式主要⽤于解决与同步相关的问题并避免竞争条件。
- 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作;
- 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数;
- 支持信号量组。
优点:可以同步进程;缺点:信号量有限
信号
定义
- 信号是事件发生时对进程的通知机制(软件中断),它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。
- 信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。信号处理函数都是非阻塞的。
信号的目的和特点
◼ 使用信号的两个主要目的是:
让进程知道已经发生了一个特定的事情。
强迫进程执行它自己代码中的信号处理程序。
◼ 信号的特点:
简单
不能携带大量信息
满足某个特定条件才发送
优先级比较高
◼ 查看系统定义的信号列表:kill –l
◼ 前 31 个信号为常规信号,其余为实时信号。
定时器,精度微秒us
unsigned int alarm(unsigned int seconds);
信号捕捉函数
◼ sighandler_t signal(int signum, sighandler_t handler);
◼ int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
SIGCHLD信号
◼ SIGCHLD信号产生的条件
子进程终止时
子进程接收到 SIGSTOP 信号停止时
子进程处在停止态,接受到SIGCONT后唤醒时
◼ 以上三种条件都会给父进程发送 SIGCHLD 信号,父进程默认会忽略该信号
内存映射
定义:内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。 (效率高)
内存映射相关系统调用
#include <sys/mman.h>
NULL,内核指定 数据映射的长度 读写权限 是否同步 文件描述符 偏移量
void *mmap(void *addr, size_t length, int prot, int flags, int fd,
off_t offset);
返回值:返回创建的内存的首地址
失败返回MAP_FAILED,(void *) -1
int munmap(void *addr, size_t length); //解除映射的内存
addr:要释放内存的首地址
length : 要释放的内存的大小,要和mmap函数中的length参数的值一样
使用内存映射实现进程间通信
1.有关系的进程(父子进程)
- 还没有子进程的时候
- 通过唯一的父进程,先创建内存映射区
- 有了内存映射区以后,创建子进程
- 父子进程共享创建的内存映射区
2.没有关系的进程间通信
- 准备一个大小不是0的磁盘文件
- 进程1 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 进程2 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 使用内存映射区通信
注意:内存映射区通信,是非阻塞。
注意事项
1.如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
void * ptr = mmap(...);
ptr++; 可以对其进行++操作
munmap(ptr, len); // 错误,要保存地址
2.如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
错误,返回MAP_FAILED
open()函数中的权限建议和prot参数的权限保持一致。
3.如果文件偏移量为1000会怎样?
偏移量必须是4K的整数倍,返回MAP_FAILED
4.mmap什么情况下会调用失败?
- 第二个参数:length = 0
- 第三个参数:prot
- 只指定了写权限
- prot PROT_READ | PROT_WRITE
第5个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY
5.可以open的时候O_CREAT一个新文件来创建映射区吗?
- 可以的,但是创建的文件的大小如果为0的话,肯定不行
- 可以对新的文件进行扩展
- lseek()
- truncate()
6.mmap后关闭文件描述符,对mmap映射有没有影响?
int fd = open("XXX");
mmap(,,,,fd,0);
close(fd);
映射区还存在,创建映射区的fd被关闭,没有任何影响。
7.对ptr越界操作会怎样?
void * ptr = mmap(NULL, 100,,,,,);
4K
越界操作操作的是非法的内存 -> 段错误
进程虚拟地址空间
fork()后,子进程的用户区数据会和父进程一样,内核区也会拷贝过来,
当pid不同,pid>0时为父进程,等于0时为子进程。
- 物理地址:它是地址转换的最终地址,进程在运行时执行指令和访问数据最后都要通过物理地址从主存中存取,是内存单元真正的地址。
- 逻辑地址:是指计算机用户看到的地址。例如:当创建一个长度为 100 的整型数组时,操作系统返回一个逻辑上的连续空间:指针指向数组第一个元素的内存地址。由于整型元素的大小为 4 个字节,故第二个元素的地址时起始地址加 4,以此类推。事实上,逻辑地址并不一定是元素存储的真实地址,即数组元素的物理地址(在内存条中所处的位置),并非是连续的,只是操作系统通过地址映射,将逻辑地址映射成连续的,这样更符合人们的直观思维。
- 虚拟内存:是计算机系统内存管理的一种技术,需要MMU(内存管理单元)的支持。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
进程的调度算法
定义:根据系统的资源分配策略所规定的资源分配算法。
-
先来先服务 first-come first-serverd(FCFS)
非抢占式的调度算法,按照请求的顺序进行调度。
有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。 -
时间片轮转调度算法
将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。时间片轮转算法的效率和时间片的大小有很大关系:
(1)因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。
(2)如果时间片过长,那么实时性就不能得到保证。 -
短作业优先 shortest job first(SJF)
非抢占式的调度算法,按估计运行时间最短的顺序进行调度。
长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。 -
最短剩余时间优先调度算法
最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。
孤儿进程,僵尸进程,守护进程
孤儿进程
父进程运行结束,但子进程还在运行(未运行结束),
这样的子进程就称为孤儿进程(Orphan Process)。
◼ 每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init
进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束
了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。
◼ 因此孤儿进程并不会有什么危害
僵尸进程
- 每个进程结束后,都会释放自己的地址空间中的用户数据,内核中的PCB没有办法自己释放,需要父进程进行释放。
- 进程终止后,父进程尚未回收,子进程残留资源PCB存放在内核中,变成僵尸(zombie)进程
- 僵尸进程不能被kill -9 杀死
- 导致结果,父进程不调用wait()和waitpid(),保留的信息就不会被释放,进程号一直被占用,系统的进程号是有限的。如果有大量僵尸进程,会导致系统没有可用的进程号而不能产生新的进程号
守护进程
Daemon 进程是 Linux 中的后台服务进程(后台一直运行)
守护进程是一个生存期较长的进程,通常独立于控制终端并且周
期性地执行某种任务或等待处理某些发生的事件。
守护进程具备下列特征:
- 生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭。
- 它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进
程自动生成任何控制信号以及终端相关的信号(如 SIGINT、SIGQUIT)。
页面置换算法
先进先出置换算法(FIFO)
先进先出,即淘汰最早调入的页面。
最佳置换算法(OPT)
选未来最远将使用的页淘汰,是一种最优的方案,可以证明缺页数最小。
最近最久未使用(LRU)算法
即选择最近最久未使用的页面予以淘汰
时钟(Clock)置换算法
时钟置换算法也叫最近未用算法 NRU(Not RecentlyUsed)。该算法为每个页面设置一位访问位,将内存中的所有页面都通过链接指针链成一个循环队列。
多进程模型
-
为每个客户端分配一个进程来处理请求。
-
服务器的主进程负责监听客户的连接,一旦与客户端连接完成,accept() 函数就会返回一个「已连接 Socket」,这时就通过 fork() 函数创建一个子进程,实际上就把父进程所有相关的东西都复制一份,包括文件描述符、内存地址空间、程序计数器、执行的代码等。
-
根据返回值来区分是父进程还是子进程,如果返回值是 0,则是子进程;如果返回值是其他的整数,就是父进程。
◼因为子进程会复制父进程的文件描述符,于是就可以直接使用「已连接 Socket 」和客户端通信了。
◼子进程不需要关心「监听 Socket」,只需要关心「已连接 Socket」;父进程则相反,将客户服务交给子进程来处理,因此父进程不需要关心「已连接 Socket」,只需要关心「监听 Socket」。
可能出现的问题: -
当「子进程」退出时,实际上内核里还会保留该进程的一些信息,也是会占用内存的,如果不做好“回收”工作,就会变成僵尸进程,随着僵尸进程越多,会慢慢耗尽我们的系统资源。
◼父进程要“善后”好自己的孩子,怎么善后呢?那么有两种方式可以在子进程退出后回收资源,分别是调用 wait() 和 waitpid() 函数。
◼进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。