进程控制
-
fork 之后,父、子进程,共同争夺cpu ,执行先后顺序随机。
-
ps aux | grep 关键字。 —— 搜索系统中包含关键字的进程。
fork后父子进程异同
“刚刚” fork 后:
父子相同:
-
全局变量、.data、.text、栈、堆、环境变量、用户ID、进程工作目录、宿主目录、信号处理方式...
父子不同:
-
进程ID、fork返回值、进程运行时间、父进程ID、闹钟(定时器)、未决信号集。
读时共享,写时复制
-
fork后,对于父进程的用户空间的数据,系统采用 ==读时共享,写时复制== 原则。
-
重点常用:全局变量。
fork后父子进程共享
-
文件描述符(对应 打开的文件结构体)
-
mmap 创建的映射区。
进程回收
-
fork后的子进程,其父进程有义务在子进程结束时,回收该子进程pcb。隔辈进程无回收关系。
-
进程终止:
-
关闭所有文件描述符
-
释放用户空间分配的内存。
-
进程的 pcb 残留在内核。保存进程结束的状态(正常:退出值。异常:终止其运行的信号编号)
-
孤儿进程
父进程,先于子进程终止。子进程沦为 “孤儿进程”。会被 init 进程领养。
使用命令:ps ajx 查看 ppid(父进程id)pid(进程id)gid(进程组id)sid(会话id)
僵尸进程
子进程终止,父进程未终止,但尚未对子进程回收。在此期间,子进程为 “僵尸进程”。
杀死进程命令:kill -9 进程id 。 只能杀死活跃的进程,对僵尸进程无效!!!
wait回收
-
只有 “父、子” 进程之间存在,回收关系。 爷孙进程、兄弟进程、叔侄进程... 不存回收关系!
#include <sys/wait.h> pid_t wait(int *wstatus); 参: 传出参数。回收进程的状态。传 NULL,只回收进程的pcb,不获取退出状态。 返回值: 成功:回收的进程pid 失败:-1, errno
-
函数的作用:
-
阻塞等待子进程退出(终止)。
-
回收子进程残留在内核的 pcb。
-
获取子进程的退出状态(正常、异常)。—— 传出参数 :wstatus
-
-
回收子进程退出状态:
-
正常退出:
-
判断 WIFEXITED(status) 为真。
-
进一步使用 WEXITSTATUS(status) 获取退出值。
-
-
异常退出:
-
判断 WIFSIGNALED(status) 为真。
-
进一步 使用 WTERMSIG(status) 获取杀死子进程的信号的编号。
-
-
waitpid回收
pid_t waitpid(pid_t pid, int *wstatus, int options); 参数: pid: >0: 通过pid指定 回收某一个子进程。 -1: 回收任意子进程。 0: 回收 与父进程属于同一个进程组的 子进程。 —— 子进程创建成功后,默认,会自动加入父进程进程组。 wstatus:(传出)回收子进程状态。 --- 类似于 wait() 的参数。 options:WNOHANG —— 指定回收方式为 “非阻塞”。 0 —— 指定回收方式为 “阻塞”。等同于 wait() 返回值: > 0: 表成功回收的进程pid 0: 函数调用时参3指定了 WNOHANG,子进程没有结束。 -1:失败。 errno
进程间通信 IPC
-
进程间通信的原理,借助 多个进程使用同一个 内核,借助内核,传递数据。
进程间通信的方法
-
管道:最简单。
-
信号:开销小。
-
mmap映射:速度快,非血缘关系间。
-
socket(本地套接字):稳定性好!
管道pipe
-
实现原理:Linux 内核 使用环形队列机制,借助缓冲区(4k)实现。
-
特质:
-
本质:伪文件(实为内核缓冲区)
-
用于进程间通信,右两个文件描述符引用,一个读端,一个写端。
-
规定,数据从管道写端流入,从读端流出。
-
-
局限性:
-
自己写,不能自己读。
-
管道中的数据,读走就没!不能反复读取!
-
半双工通信。(对讲机)
-
应用于血缘关系进程间。
-
pipe管道读写行为
-
读管道:
-
管道有数据, read返回实际读到的字节数。
-
管道无数据,1)无写端,read 返回 0 (类似读到文件末尾)
2)有写端,阻塞等待。
-
-
写管道:
-
无读端,异常终止。 ( SIGPIPE 信号)
-
有读端,1)管道已满,阻塞等待。
2)管道未满,返回实际写出的字节数。
-
pipe管道的优劣
-
优点:简单。比信号、套接字、mmap 简单很多!
-
缺点:
-
只能单向通信,实现双向通信,需要两个管道。
-
只能应用于父子、兄弟...(有公共祖先)进程间同。无血缘关系进程间,后来用 fifo 替代。
-
命名管道 fifo
-
命令创建:mkfifo 管道名。
-
函数创建:mkfifo
int mkfifo(const char *pathname, mode_t mode);
-
可以用于无血缘关系进程间通信。 —— 演示代码:fifo_w.c fifo_r.c
-
管道中的数据,一次性读取,读走没。
-
读端:以 O_RDONLY 打开 fifo 管道。
-
写端:以 O_WRONLY/O_RDWR 打开同一个 fifo 管道。
mmap
文件进程间通信
-
有血缘关系、无血缘关系的进程,都可以使用 ==同一个文件== 来实现进间通信。
建立、释放映射区
-
mmap 借助文件映射,创建共享内存映射区。
mmap使用的注意事项
-
用于创建映射区的文件的大小,必须是非0。映射区的大小 <= 文件大小。
-
创建映射区,需要read权限。指定访问权限为 MAP_SHARED, mmap需要读写权限。 应该 <= 文件打开权限。只写不行!
-
文件描述符fd, 在mmap创建映射区完成,可以立即关闭!后续访问文件使用 内存地址。
-
offset 必须是 4096 的整数倍。(MMU映射的最小单位 4k)
-
映射区访问的权限设为 MAP_PRIVATE, 对内存做的所有修改,都只在内存有效,不反应的磁盘上。不能应用于 IPC
进程间通信
父子进间通信
-
父进程 先创建 映射区。 open(O_RDWR); mmap(MAP_SHARED);
-
fork() 创建子进程。
-
一个进程使用 映射区读,另一个进程写。
无血缘关系进程间通信
-
两个进程,打开同一个文件,创建映射区。
-
指定 flags 为 MAP_SHARED。
-
一个进程使用 映射区读,另一个进程写。
mmap特性
-
fifo、mmap 都可以应用于非血缘关系进程间通信。区别:
-
mmap:数据可以重复读写。
-
fifo:数据只能一次性读写。
-
-
直接操作内存,执行速度快!
信号
信号描述
-
信号的共性:
-
简单。
-
不能携带大量数据。
-
满足某一特定条件才发送。
-
-
信号的特质:
-
信号软件层面上的 “中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止,处理信号,处理结束后,再继续执行后续指令。
-
所有的信号,产生、处理都是由内核完成。
-
信号的实现手段导致,信号有很强的延时。对用户而言,依然感觉不到。
-
信号相关的概念
-
未决:
-
产生与递达(处理)之间的状态。 该状态主要受 阻塞(屏蔽)影响。
-
-
递达:
-
内核产生信号后递送并且成功到达进程。递达的信号,会被内核立即处理。
-
-
信号处理方式:
-
执行默认动作。
-
忽略(丢弃)。
-
捕捉(调用用户指定的函数)。
-
-
阻塞信号集(信号屏蔽字):
-
本质:位图。用来记录信号的屏蔽状态。
-
该信号集中的信号,表示成功被设置屏蔽。再次收到该信号,其处理动作将延后至解除屏蔽。此期间该信号一直处于未决态。
-
-
未决信号集:
-
本质:位图。用来记录信号的处理状态。
-
该信号集中的信号,表示,信号已经产生,但尚未被处理。
-
信号4要素
-
信号使用之前,==“必须”== 先确定该信号的 4 要素,再使用!
-
四要素内容:
-
编号
-
名称
-
事件
-
默认处理动作 ( 参考讲义“Linux常规信号一览表” 或者 APUE 10.2 章)
-
-
使用命令 kill -l 查看Linux 系统中,支持的所有信号。1~31 号:常规信号。
-
man 7 signal 查看信号 4 要素。
-
9) SIGKILL 和19) SIGSTOP 信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。
信号的产生
-
按键产生
-
Ctrl + c → 2) SIGINT(终止/中断)
-
Ctrl + \ → 3) SIGQUIT(退出)
-
-
系统调用产生
-
alarm() → 14) SIGALRM
-
abort()
-
raise()
-
-
软件条件产生
-
alarm() → 14) SIGALRM
-
setitimer() → 14) SIGALRM(计时)
-
-
硬件异常产生信号
-
段错误:内存访问异常 。→ 11) SIGSEGV
-
浮点数例外:除 0。 → 8) SIGFPE
-
总线错误。内存对齐出错。 → 7) SIGBUS
-
-
命令产生。
-
kill 命令
-
进程组和会话
-
进程聚集成为进程组,多个进程组聚集成为会话。
守护进程
守护进程daemon概念
-
Linux后台的一些服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在后台运行。周期性的执行某种任务,或者等待某一事件的发生。 一般采用以d 结尾的命名方式。
线程
线程的概念
-
Linux 系统中,线程 称之为:轻量级的进程。
-
进程:有独立的进程地址空间, 有独立的 pcb。 —— 最小资源分配单位。
-
线程:有独立的pcb,没有独立的进程地址空间。(与其他线程共享) —— 最小执行单位。
-
一个创建了线程的进程,本身也沦落 为线程。
-
LWP 号: cpu 划分时间片依据。 —— 线程 最小执行单位。
-
查看LWP号命令: ps -Lf 进程pid
-
线程内核三级映射
-
三级映射。—— 解释了,为什么线程没有独立的进程地址空间。
-
《Linux内核源代码情景分析》 ----毛德操
线程共享
-
独享:栈空间(用户栈、内核栈)errnum
-
共享:./text ./ordata ./data ./bss heap堆 ----> 共享全局变量(可以创建堆区,能容直接共享到其它线程)
线程优缺点
-
优点:
-
并发性强。
-
开销小。(共享一份地址空间)
-
数据通信方便。(不需要IPC通信,因为共享全局变量和堆区)
-
-
缺点:
-
库函数,稳定性差。
-
调试、编写困难(gdp不支持)
-
对信号支持差。
-
-
结论:既能使用进程开发,也能使用线程开发的程序,首选 线程。
线程同步
-
同步:即协同步调,按预定的先后次序访问共享数据。
-
线程同步,指一个线程发出某一功能调用时,没有得到结果之前,该调用不返回。同时,其他线程为保证数据一致性,不调用该功能。
-
数据混乱产生的原因
-
资源共享
-
调度随机
-
线程之间缺乏必要同步机制。
-
-
只能从第3步着手解决 “数据混乱”, 避免产生与时间有关的错误。执行多个控制流(线程)访问共享的数据的先后顺序。
-
锁的属性:
-
系统提供内用户使用的所有的锁,全部为 “建议锁”, 不具备强制性。
-
如果访问共享数据的线程,不拿锁,直接访问共享数据,能访问成功。但是,数据就有出现混乱的风险。 —— 拿锁与否完全取决于编写的程序。
-
互斥量(互斥锁)
pthread_mutex_t mutex; 创建互斥锁 int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr); 初始化互斥锁 int pthread_mutex_lock(pthread_mutex_t *mutex); 加锁, 如果锁被占用,阻塞等。 int pthread_mutex_trylock(pthread_mutex_t *mutex); 尝试加锁,不阻塞。 int pthread_mutex_unlock(pthread_mutex_t *mutex); 解锁, 唤醒阻塞在此把锁上的线程。 int pthread_mutex_destroy(pthread_mutex_t *mutex); 销毁互斥锁
-
注意事项:
-
保证锁的粒度 , 越小越好!( 访问共享数据前,加锁,访问共享数据结束,立即解锁。)
-
互斥锁,本质:结构体。 我们可以把它当成整数看待。 初值为 1(init成功)。 取值:1 或 0
-
加锁:--操作。1-- ---> 0 。 失败:阻塞线程。
-
解锁:++操作。0++ ---> 1。 成功:唤醒阻塞在锁上的线程。
-
try锁:尝试加锁。 成功--。 失败,设置错误号 EBUSY、EINVAL
-
-
初始化互斥量
pthread_mutex_t mutex; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 静态初始化。--- 常用于全局变量 pthread_mutex_init(&mutex, NULL); // 动态初始化。--- 常用于函数内部初始化。
死锁
-
死锁不是一种锁!是错误使用锁的一种状态。
-
常见死锁种类:
-
对一把锁,反复lock。
-
两个线程, 各自持有一把锁,请求另一把锁。
-
读写锁
3句话描述
-
锁,只有一把!可以指定为 “读模式” 和 “写模式”
-
读共享,写独占。
-
写锁优先级高!( 读锁已经加锁成功,写锁无法加锁成功,阻塞等。)
-
相较于互斥锁而言,读写锁非常适合于对数据结构读的操作次数,远大于写的操作次数的场景。
信号量(semaphore)
-
信号量与信号无关!
-
信号量 相当于 初始值为 N 的互斥量。 N值,表示可以同时访问共享数据的线程数。
操作函数
#include <semaphore.h> sem_t sem; // 创建信号量对象 int sem_init(sem_t *sem, int pshared, unsigned int value); //动态初始化方法。没有静态 参2 pshared: 0: 用于线程间同步 非0(1): 用于进程间同步 参3 value:信号量的初值。 N值。代替同时访问共享数据的线程个数。 返回值: 成功:0 失败:-1, errrno --- perror() int sem_destroy(sem_t *sem); int sem_trywait(sem_t *sem); int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); struct timespec { time_t tv_sec; /* Seconds */ long tv_nsec; /* 纳秒 Nanoseconds [0 .. 999999999] */ }; abs:绝对。 绝对时间:从 1970年1月1日 00:00:00 开始,计算时间。 int sem_wait(sem_t *sem); 一次调用,做一次--操作。当信号量的值为0时, 再次--,会阻塞。(对比pthread_mutex_lock) int sem_post(sem_t *sem); 一次调用,做一次++操作。当信号量的值为N时, 再次++,会阻塞。(对比pthread_mutex_unlock) Link with -pthread. // 编译时,指定线程库