在代码中启动多个进程
使用system库函数启动多个进程
传统的进程调用就是我们在命令框里输入运行某个进程,而我们可以依靠代码,实现让一个进程取启动另一个进程
在进程运行过程我们使用命令ps -elf
看到正在运行的有三个进程
system的调用过程
首先./system
进程通过system
库函数创建sh -c
进程,在通过sh -c
进程来启动./sleep
进程,因此就有上面说的有桑格进程在运行,然后./sleep
进程运行结束,跳回sh -c
进程,进程在结束,跳回./system
进程执行后续操作
fork系统调用启动多个进程
在执行了
fork
指令之后,进程就会将自己复制一份一摸一样的进程出来,里面的代码段,pc指针都相同,这两个进程一个是父进程,一个是子进程,在父进程和子进程中已执行指令和未执行执行指令都是相同的,这个时候如果我们不加以控制这两个进程就会并行的执行相同的指令。
父进程中的fork
的放回值是子进程的pid
,子进程中的fork
返回值是0,这样我们就可以按照其返回值进行判断,让不同的进程走向不同的分支,实现不同的代码
我们可以看到父进程由命令行(bash分支)创建
深入fork
fork实现的底层原理
(1)fork
调用的时候回拷贝一份task_struct
(2)子进程修改一些必要数据
(3)加入到就绪队列
第一步和第二步不可抢占,时间尽可能短,我们称为进程调用的上半部;第三步是可以抢占的,执行这个操作的时间可以长,我们称为下半部
系统调用是怎样实现的
用户态下不能执行所有的指令,因此cpu的使用状态分为用户态和内核态
需要调用硬件的功能或者硬件发生一些事件,此刻cpu状态就会处于内核态进行处理
硬件中断(用户态—>内核态)
fork的性能
上面那我们说过在执行fork
指令之后,进程会复制一个和自己完全一样的进程出来,那么复制出来的进程和我们原来的进程是怎么使用内存空间的呢,这是一个问题,因此fork
在进行复制时,其实不会给新的进程分配物理空间,当我们只进行读取操作这样对两个进程都没有影响,但是当某个进程执行写操作,需要修改进程内存中的数据,就会在复制一个修改数据之前的数据块,分配物理内存,让另外一个进程指向这个内存空间
fork的拷贝
在逻辑上,父子进程的用户态空间(栈,堆,数据段)是拷贝的
在代码中可以看到我们先让父进程睡眠等待子进程输出完毕以及更改数据完毕再执行父进程,但是我们再子进程中修改的数据只影响了子进程中的输出,并没有改变父进程中的数据输出,再次验证了用户态空间(栈,堆,数据段)不是共享的,是拷贝的
使用fork和wait手动实现system库函数
FILE的拷贝
printf
的本质:往stdout
中写入内容,遇到换行\n
或者缓冲区满的时候将数据拷贝到内核文件对象中
内核态是拷贝还是共享
对于文件对象父进程和子进程是共享的,标准输出输入设备父子进程也都是共享的
exec(系统调用)函数族
exec
将一个可执行程序加载到本能地进程的地址空间
调用exec
会清空数据(栈,堆,数据段),将函数参数中的pathname
加载进来,取代原来的代码段,重置PC
指针
图中标注出来的是需要重点掌握的函数execl l是指list,是可变参数
,execv v是指vector元素为指针的数组
execl
参数含义:pathname
指明可执行参数的路径;第二个或者更多的参数表示要调用程序所需要携带的参数,NULL
值表示参数输入完毕,我们也可以看到我们执行了两个程序,但是只使用了一个进程
execl用法
execv用法
以上的方法也可以用system
函数实现
使用strtok分割字符串
第一个参数是传入传出参数,只能使用字符数组,不能只用字面值
wait
在上面使用fork
将进程复制为父子进程的示例中,我们可以看到我们在执行父进程程序之前我们是先对父进程使用sleep(1)
让父进程睡眠1秒
在执行,那我们为什么要给父进程执行sleep(1)
指令呢,如果不执行又是什么情况,下面是如果不对不父进程执行sleep(1)
的输出结果。
看输出结果父子进程的pid
没有问题,但是(1)子进程的ppid
有问题;(2)以及子进程打印信息的位置有问题,现在子进程打印的位置在命令提示行之后,显示错乱。并且我们可以看到程序先打印了父进程的指令,再打印执行子进程程序,这样导致子进程执行完毕要回收资源时找不到父进程
这就涉及到进程的退出;在
linux
里进程退出之后其资源的回收由其父进程回收(调用wait )
我们可以使用wait
函数让父进程在子进程销毁之后为子进程回收资源,这样进程的运行又回归正常
子进程未终止,父进程已终止;这样的进程我们称之为孤儿进程,他们需要重新找其他进程作为自己的父进程,一啊不能都是找1
进程作为父进程
子进程终止的时候,父进程一直不调用wait
,此时进程已经死亡,但是资源还没回收,这样的进程我们称之为僵尸进程
使用wait获取子进程的退出状态
可以根据下面的宏来检测是否为正常退出
可以获取我们的返回值进行放回
我们用9
号信号杀死进程,便会打印出其时非正常退出
wait
的缺陷:假如一个父进程有多个子进程,那么wait
只能等一个子进程死
waitpid
options
可以设置属性,可以设置的值WNOHANG
,WNOHANG
的作用,过一段时间回来查看以下子进程是否死亡,如果死亡就为子进程回收资源,如果没死那么在过一会再来看,
如果加上WNOHANG
属性,那么如果子进程终止返回0
;如果进程已终止,就会回收资源
非阻塞通常配合循环使用
pid
的值如果是-1
,那么就是可以等待任何一个子进程
同步
:事件发生的时间顺序总是确定固定的
异步
:某件事件发生之后另一个事件不一定执行
进程的正常终止
(1)在main函数中调用return,可以使用echo $?
查看返回值
好处:写法方便
坏处:只能退出当前函数,不能退出进程
(2)使用exit(number)
可以在进程的任何时刻都可以退出进进程,其中number
是其退出进程的返回值
并且我们可以看到printf
没有换行符\n
,因此打印的数据是hello
存储在标准输出stdout
文件流里面的,并没有存储在文件对象里面,所以exit()
可以帮我么清空文件流,并且将数据显示在屏幕上
如果使用printf
加上了换行符\n
那么打印的数据就会存储到文件对象中
_exit()
作用和exit()
的作用是一样的,但是其不会自动清空文件缓冲区数据(标准输出stdout
)
_exit()
和_Exit()
的作用是一样的
进程异常终止
(1)主动异常终止
6
号信号,自杀信号
(2)另一个进程/硬件发信号终止
进程组
进程组是进程的集合,每个进程只能属于一个进程组,组ID
是组长的PID
,父子进程属于同一个进程组
即使组长进程终止,组ID
也不变
新进程的PID
不和组长ID
重复
普通组员可以脱离原来的组创建新的组,但是组长不行
获取组ID
和设置组ID
getpgid(pid)
中的参数pid
是指你要获取的进程的pid
,如果参数值为0
,那么返回值为本进程的父进程PID
通过shell
启动的进程是一个新的进程组的组长
因此此进程一已经不能在重新建立新的组
但是其子进程可以创建新的进程组,并且这不会影响之前的父子关系
setpgid(0,0)
两个参数的意义,第一个参数表示我们要修改谁的进程组ID
,第二个参数表示我们要将目标进程的进程组ID
改为多少
因此两个0
表示要将本能进程设为新的一个进程组组长
在一个终端(会话)中,有至多一个前台进程组,有任意个后台进程组
使用会话session管理进程组
会话是进程组的集合,创建会话的进程我们称之为会话首进程
,会话首进程必定是组长,其也是会话的第一个进程
会话可以连接一个终端,如果有终端连接会话,就会有一个专门的进程去和会话进行交互,这个进程我们称之为控制进程
如果终端关闭,所有会话内的进程会收到一个断开连接信号
获取会话
ID
参数为我们我们想要获取的进程PID
,如果参数为0
则返回当前进程的会话ID
更改会话ID
,实质不是区=去更改会话ID
是拿当前的的进程创建新的会话
守护进程daemon
即使是会话(终端)关闭,进程依然可以持续运行,因此守护进程是孤儿进程
守护进程一般以d
结尾,例如sshd
就是一个守护进程
守护进程特点:
(1)创建新的会话(将父进程终止,重新创建会话)
(2)重置掉当前工作目录pwd
和文件创建掩码umask
(3)关闭所有的文件描述符(因此如果守护进程需要输出数据只能输出到日志系统,可以和日志系统进行交互)
守护进程的使用
日志系统末尾打印出我们的日志数据
日志系统
日志系统本质就是一个可写入文件,并且会记录其优先级priority
下面就是一些优先级参数
进程间通信(Inter Process Communication ---- IPC)
目的打破进程之间的隔离,从而使得进程之间可以共享数据
IPC包括:
(1)管道
–重要;(2)共享内存;(3)信号量;(4)消息队列;(4)信号
–重要
有名管道:在文件系统中存在一个管道文件
创建有名管道
删除一个管道
改变名字或者位置和mv
实现的功能一样
创建硬链接
匿名(无名)管道:在文件系统中不存在管道文件,只用于父子进程之间
管道用法popen(库函数)
进程执行popen
库函数之后会创建一个子进程,父子进程之间用管道连接在一起,管道两端是文件流
popen
库函数的第二个参数type
的值可以为w
或r
;w
表示父进程
可写
入数据到管道文件流FILE
内,子进程把自己的stdin
重定向为管道;r
表示父进程
可读
取管道文件流FILE
,子进程把自己的stdout
重定向为管道;
读模式
写模式
pipe系统调用
pipe
可以在一个进程的内核态创建两个文件对象,因此需要有两个文件描述符去指向这两个文件对象,这两个文件对象分别对应着管道的读端和写端,因此我们需要有两个整形分别为pipefd[0]
pipefd[1]
这也就是下面的pipe
函数参数为pipefd[2]
,表示参数应该是一个长度为2
的int
数组
此进程通过pipe
系统调用向自己的写管道文件中写入数据,此时写入的数据就会在读取管道中就可以实现读取
那么这样子的进程自己输入自己输出有什么用呢,这看上去没什么用,但是假如我们使用pipe
之后再加上使用fork
操作复制一个子进程,此时文件对象是共享的,因此子进程也会复制相同的文件描述符,指向这两个读写文件对象,此时父子进程可以通过pipe
进行通信(半双工通信)
共享内存
让不同进程的虚拟内存页对应同一个物理页框
共享内存是效率最高的进程通信IPC
库文件经常使用共享内存,使用lsof
命令可以看到加载到内存的库文件
System V的共享内存机制
ftok
将文件名转换为一个实现进程间通信找到共享内存的key
我们就可以通过上面获取到的key
使用shmget
去构建一个共享内存
我们可以使用ipcs
指令去查看我们创建的共享内存
尽管上面我们创建好了共享内存,但是我们的进程是无法使用的还只是虚拟内存,我们还需要将共享内存加载到我们的物理内存中,才能让我们使用
shmat
函数的第一个参数shmid
就是上面shmget
函数的返回值,第二个参数我们填写NULL
那么就会自动分配虚拟内存空间进行映射,最后一个参数我们填上0
,代表什么事情都不用做
shmdt
函数就是对共享空间进行回收,回收的是虚拟内存
共享内存的用法,当我们创建共享内存之后,共享内存块内的初始值会置为0
不同进程只要通过相同的key
就可以对共享空间进行读写
私有共享内存
如果我们将key
的值改为IPC_PRIVATE
(这个宏的值为0),则会创建一个私有共享内存,此内存只能父子进程才能进行访问,只能实现父子进程之间的通信
如果我们不让子进程睡眠1秒,那么有可能打印不出任何消息因为子进程和父进程是同步进行的,可能父进程还没有向共享内存里写数据,子进程就已经执行输出指令
在上面的代码中我们可以看到我们将共享内存指针定义为
int
类型,然后再让父子进程向共享你内存中写入数据(执行假发操作),但是输出的结果却小于我们预测的结果(2000000)
这是由于竞争条件
引起的
两个进程并发的访问共享资源,虽然上面我们只是执行了一句简单的p[0]++
但是当其转换为汇编代码就会编程分为三步进行执行
(1)将p[0]
数据mov
写到寄存器
(2)寄存器将数据进行加法操作
(3)寄存器 将数据mov
写回p[0]
在这三个步骤执行的过程中,很有可能发生时钟中断,因此就会发生数据丢失,导致在寄存器里已经执行加一操作的数据没有写回p[0]
,这就是竞争条件
在多个进程去同时读取一个共享内存空间时,就会发生资源丢失
shmctl
对共享内存进行管理
参数
cmd
取值
IPC_STAT
获取状态
IPC_SET
修改状态
IPC_RMID
删除
这三个变量能够让函数执行不同指令,该函数违背了“单一职责”原则参数
shmid_ds *buf
的结构体
shm_perm
权限结构体
shm_segsz
大小
shm_atime
上次连接时间
shm_dtime
上次结束连接的时间
shm_ctime
修改的时间
shm_cpid
创建者的pid
shm_lpid
上次连接和解除连接的pid
shm_nattch
当前共享内存有多少进程连接
设置和修改属性,修改属性之前必须先IPC_STAT
删除共享内存IPC_RMID
这个操作并不会真正的删除共享内存,如果共享内存还有其他进程正在连接,那么就会等到其他进程全部断开连接,才会真正删除共享内存,标记共享内存将要删除
最右边是执行期间在sleep
时打印的,期间因为进程在睡眠,因此末尾只有一个dest
的删除标记,但是由于此共享内存还有进程在连接,因此不会删除这个共享内存
中间的是在执行完毕之后打印的