进程的创建、控制、进程间通信等
程序和进程
进程是正在运行的程序的实例,是操作系统动态执行的基本单元
可以使用一个程序创建多个进程
单道和多道
单道技术:计算机内存中只允许一个程序运行
多道技术:为提高cpu利用率,允许同时在计算机内存中存放几道相互独立的程序,使他们在管理程序的控制下,相互穿插运行,共享系统资源
时间片
操作系统分配给每一个正在进行的进程微观上的一段cpu时间
并行和并发
并行(Parallel):同一时刻有多条指令在多个处理器上同时进行
并发(Concurrent):同一时刻只能由一条指令执行,但是多个进程指令被快速的轮换交替执行,使其看起来使多个进程同时执行
进程控制块(PCB)
内核为进程所做的事情进行清楚地描述,linux内核的进程控制块使task_srtuct结构体(位于/usr/src/linux-headers-xxx/include/linux/sched.h)
主要包含:进程id,进程状态,进程切换时需要保存或回复的cpu寄存器,描述虚拟地址空间的信息,描述控制终端的信息,当前工作目录,umask掩码,文件描述符表,信号量,用户id组id,会话和进程组,进程资源上限等
进程状态转换
三态模型:就绪态,运行态,稳定态
五态模型:新建态,[三态模型],终止态
就绪态:进程具备运行条件,等待分配处理器,系统会形成一个就绪队列
运行态:进程占用处理器正在运行
阻塞态:又叫等待态/睡眠态,进程不具备运行条件或等待某一事件完成
如何查看进程状态
1 | ps aux / ajx |
进程创建
1 |
|
原理
父子进程均独立拥有PCB,拷贝后内存块内用户区内容一致,内核区会有所区别
实际上:fork是通过读时共享,写时拷贝实现的,资源在fork后并不立即拷贝,此时为只读共享状态,只有在写入时才会复制地址空间
但值得注意的是:刚被创建出,两个进程均未执行任何写操作时,父子进程共享文件表,相同的文件描述符指向相同的文件表,引用计数增加
gdb调试
gdb默认只跟踪一个进程,可以在调用前设置跟踪某一进程
1 | set follow-fork-mode [parent|child] |
默认为on,表示调试当前进程时其他进程继续运行
info inferiors 查看当前调试的所有进程
inferior [id] 切换当前调试的进程
注意:需要通过info inferiors查看id,切换时需要输入的是id,不是pid
detach inferiors [id] 使进程脱离gdb调试独立运行
exec函数族
作用:根据指定的文件名找到可执行文件,然后替换调用进程中的内容,一般会创建子进程,然后在子进程中执行exec,子进程中所有的代码都会被替换,即便exec的内容结束,也并不会执行之后的任何代码
参数 | 指代 | 作用 |
---|---|---|
l | list | 参数地址列表 |
v | vector | 存有参数地址的指针数组的地址 |
p | path | 按path环境变量指定的目录搜索可执行文件 |
e | environment | 存有环境变量字符串地址的指针数组的地址 |
最初的exec版本: |
1 | int execve(const char *filename,char *const argc[],char *const envp[]); |
其他的所有exec族都是对execve的封装
进程控制
1 |
|
孤儿进程
定义:父进程终止运行,但仍在运行的子进程
孤儿进程的附近成为init,init会循环wait()它的已经退出的子进程,当孤儿进程结束时,init会代为处理善后(调用_wait())
注意:在部分发行版中,孤儿进程并不一定被init收养,但等效
僵尸进程
进程结束之后,会释放用户区数据,但是PCB是由父进程释放的,如果进程终止时父进程没有回收PCB,则会造成占用进程号的僵尸进程出现
僵尸进程不可以被kill -9杀死
解决方案:让父进程死亡,让init接手该进程,会自动回收
wait函数
1 |
|
进程间通信(IPC)
目的:数据传输、通知事件、资源共享、进程控制等
linux中进程通信的方式(共7种):
同一主机进程间通信
- Unix进程间通信方式
- 匿名管道
- 有名管道
- 信号
- System V进程间通信方式/POSIX进程间通信方式
- 消息队列
- 共享内存
- 信号量
不同主机间进程通信方式
- Socket套接字
匿名管道
匿名管道是UNIX系统IPC的最古老形式
管道的特点:
- 是一个在内核内存中维护的缓冲器,存储能力有限,在不同操作系统中大小不一定相同
- 可以读写,匿名管道没有文件实体,有名管道由文件实体但不存储数据,可以按照操作文件的形式操作管道
- 一个管道是一个字节流,不存在消息或边界的概念,可以读取任意大小的数据块,不管写入的数据块是多少
- 通过管道传递的顺序是顺序的,读取和写出的顺序完全一致
- 数据传输是单向的,一端可以写入,一端可以读出
- 管道读取数据的操作是一次性的,一旦读取便会被遗弃
- 匿名管道只能在具有公共祖先的进程间使用
管道的数据结构:循环队列
但是,如果管道已满仍然写,write会阻塞
1 |
|
匿名管道的读写特点
使用管道时需要注意的情况(假设都是阻塞I/O操作):
- 所有的指向管道写端的文件描述符都关闭时,即写端的引用计数为0,如果有进程从管道的读端读取数据,那么管道中所有的数据被读取以后,再次read会返回0,效果同读到文件末尾
- 上一种情况,如果有写端没有关闭,即写端的引用计数不为0,read会阻塞
- 如果读端的引用计数为0,如果有进程写数据,该进程会收到一个SIGPIPE的信号,通常会导致进程异常终止
- 如果有指向管道读端的文件描述符没有关闭,即管道的读端引用计数大于0,而持有管道读端的进程也没有从管道中读数据,此时有进程向管道中写数据,那么管道被写满时write会被阻塞,直到管道中有空位置时并成功写入时才可以返回
有名管道
相对于匿名管道:
- 提供了一个路径名与之关联,所以这个管道可以跨亲缘关系通信
- 在文件系统中作为一个特殊的文件存在,但内容存放在内存中
- 使用FIFO的进程推出之后,FIFO文件会继续保存在文件系统中
- 不相关的进程可以通过名字打开FIFO进行通信
有名管道的创建可以通过shell完成
1 | mkfifo [name] |
也可以通过函数
1 |
|
有名管道的读写特点
- 访问一端时,若另一端的引用计数为0,该进程会阻塞
- 读取时,若管道中无数据,写端全部关闭,等效于读到文件末尾
- 读取时,若管道中无数据,写端尚未全部关闭,进程会阻塞
- 写入时,若读端被全部关闭,会受到SIGPIPE信号
- 写入时,若管道已满,会阻塞
内存映射
将文件映射到内存(进程的虚拟地址空间)中,对内存的修改操作会同步操作到文件中
1 |
|
使用内存映射实现进程间通信:
- 父进程创建映射区后,创建子进程,此时父子进程共享内存映射区
- 无关联的进程,通过同一个磁盘文件创造内存映射区即可
匿名映射
不需要文件实体的内存映射
限制:只能存在于父子进程之间
1 | void *mmap(NULL,1024, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS,-1,0); |
信号
又被称为软件中断/硬中断,是一种异步通信的方式,可以由一个进程直接中断另一个进程,通常源自于内核
目的
- 让进程知道已经发生了特定的事件
- 强迫进程执行代码中的信号处理程序
特点
- 简单
- 不能携带大量信息
- 满足特定条件发送
- 优先级高
使用kill -l查看系统定义的信号列表
前31个信号为常规信号,其余为实时信号
信号的处理
查看信号的详细信息
1
man 7 signal
默认处理方式
- Term 终止
- Ign 忽略信号
- Core 终止,生成Core文件(核心转储文件)
- Stop 暂停当前进程
- Cont 继续执行当前被暂停的进程
信号的状态
- 产生
- 未决
- 送达
SIGKILL和SIGSTOP不可以被捕捉、阻塞或是被忽略,只能执行默认动作
信号相关的函数
1 | int kill(pid_t pid, int sig) |
1 | int raise(int sig); |
1 | void abort(void); |
1 | unsigned int alarm(unsigned int seconds); |
1 | int setitimer (int which,const struct itimerval *new_val,struct itimerval *old_value); |
1 |
|
信号集
作用:存在于PCB中,表示一组不同的信号,其数据类型为sigset_t
相关函数如下:
1 | //以下的函数都是对自定义信号集进行操作 |
信号捕捉与信号集处理
1 | int sigprocmask(int how,const sigset_t *set, sigset_t *oldset); |
SIGCHLD信号
产生条件:
- 子进程终止时
- 子进程接收到SIGSTOP信号时
- 子进程处在停止态,接收到SIGCONT唤醒时
父进程默认忽略该信号,可用于解决僵尸进程的问题
共享内存
允许两个或多个进程共享物理内存的同一片区域,一个共享内存段会被称为进程用户空间的一部分,无需内核介入
使用方法
- 调用shmget()创建一个新的共享内存段,返回一个共享内存标识符,也可以获取已经存在的标识符,会自动初始化内存中数据为0
1
2
3
4
5
6
7
8
9
10
11
12int shmget(key_t key, size_t size, int shmflg);
/**
参数:
-key 通过这个值找到或创建一个共享内存
-size 共享内存的大小
-shmflg 访问权限,并上八进制权限
-IPC_CREAT 创建新内存段
-IPC_EXCL 和creat一起使用,如果内存已经存在,该调用会失败
返回值:
成功返回>0,为共享内存引用ID
失败返回-1
*/ - 使用shmat()附上共享内存段,该端课称为调用进程的虚拟内存的一部分
1
2
3
4
5
6
7
8
9
10
11
12void *shmat(int shmid,const void *shmaddr, int shmflg);
/**
参数
-shmid 共享内存的标识,即shmget的返回值
-shmaddr 申请的共享内存的起始地址,一般指定为null,由内核指定
-shmflg 对共享内存的操作
读:SHM_RDONLY
读写:0
返回值:
成功返回共享内存的首地址
失败返回(void*)-1
*/ - 使用shmdt()分离共享内存块,调用后程序无法继续引用该内存块
1
2int shmdt(const void *shmaddr)
参数:shmaddr,共享内存的首地址 - 使用shmctl()删除共享内存块,只有所有进程都与之分离后才会被销毁,只有一个进程需要执行该步骤
1
2
3
4
5
6
7
8
9
10
11
12
13int shmctl(int shmid, int cmd, struct shmid_ds *buf)
/**
参数:
-shmid 需要被删除的共享内存的id
-cmd 要做的操作
IPC_STAT 获取共享内存当前的状态
IPC_SET 设置共享内存的状态
IPC_RMID 标记被销毁
- *buf 需要设置或者获取的共享内存的属性信息
-IPC_STAT buf存储数据
-IPC_SET buf宏需要初始化数据
-IPC_RMID 设置为NULL
*/ - 使用ftok来获取key
1
2
3
4
5
6
7key_t ftok(const char *pathname, int proj_id)
/**
参数
-pathname:指定一个存在的路径
-proj_id int类型的值,系统会使用其中的1个字节,范围0-255,一般指定一个字符'a'
*/
相关系统操作命令
- ipcs
- -a 查询所有的进程间通信行为
- -m 查询所有共享内存行为
- -q 查询所有消息队列行为
- -s 查询所有管道
- ipcrm
- -M shmkey 删除shmkey创建的共享内存段
- -m shmid 删除shmid标识的共享内存段
- -Q msgkey 删除msqkey标识的消息队列
- -q msqid 删除用msqid标识的消息队列
- -S semkey 移除用semkey创建的信号
- -s semid 移除用semid标识的信号
共享内存和内存映射的区别
- 共享内存可以直接出房间,内存映射需要磁盘文件
- 共享内存效率更高
- 共享内存所有进程共享一块内存,内存映射每个进程在虚拟地址空间有一片独立的内存
- 进程突然退出,共享内存还在,内存映射消失
- 宕机,共享内存消失,内存映射会保存在文件里
生命周期
内存映射:进程退出,内存映射区销毁
共享内存:进程退出,共享内存还在,手动删除或关机