总结一下,应用层进程、应用层线程和内核线程的一些基本知识点、以及它们的通信方式、比较比较不同。
主要参考了两本书 《UNIX环境高级编程》、《Linux内核设计与实践》
每个进程都有独一无二的进程ID,ID为0:调度进程,也叫交换进程,属于内核的一部分。ID为1:Init进程(自举过程中由内核调用,自举后启动一个UNIX内核),不会终止、会成为所有孤儿进程的父进程。
fork和vfork都是创建一个新的子进程,会有两个返回值,返回值为0的是子进程、返回值不为0(子进程ID)的是父进程。其中vfork的主要目的就是创建一个新的进程执行新的程序(exec),并且它可以保证一定是子进程先运行,子进程要调用exec或者exit后,父进程才运行。如果子进程的运行对父进程有依赖,就有可能导致死锁。
①父进程运行完了,子进程还在-------> 孤儿进程
②父进程还在,子进程运行完了都是没有释放(父进程未收尸)------->僵尸进程。init进程会接手子进程,为他收尸。
③主线程推出了,子线程强制退出。
wait与waitpid用于父进程等待子进程,为他收尸(释放资源)。
调用一种exec函数时,该进程执行的程序会替换为新的程序。
①无名管道 pipe:(理论上半双工,创建两个就全双工了),只能在具有公共祖先的两个进程中使用(父进程fork前就需要pipe)。 int pipe(int fd[2]);… fd[0]为读通道、fd[1]为写通道。
popen、pclose:创建一个管道、fork一个进程、关闭未使用管道、执行shell命令,popen返回文件给父进程独或者写,用pclose关闭。
②有名管道 FIFO:不相关的进程也能够互相通信。其实就是创建了一个FIFO文件(当成普通文件即可),供所有进程一起使用。如果open的时候不加O_NONBLOCK,没数据时只读只写会阻塞,只读等只写,只写等只读。
XSI_IPC:③消息队列、④信号量、⑤共享内存,使用前都需要用ftok函数把一个路径名和id生成key_t供XSI_IPC使用。
③消息队列:与管道类似,多了一个管理队列。好处就是对每个消息可以指定消息类型,对于不同消息可以不按照队列顺序接收,可以根据自定义消息类型来接收置顶消息
④信号量:本质就是一个计数器,可以是二值信号量也可以计数信号量。为多个进程提供对共享数据的访问(同步、资源保护),原子操作。
⑤共享内存:映射一块内存,大家一起使用。为了更安全通信,往往与信号量来同步。内存映射(mmap,munmap),共享内存 shmget
⑥信号: 模拟中断机制、异步通信,分为可靠信号和不可靠信号,不可靠信号可能会丢失。对于信号的处理有三种:默认、自定义和忽略。
⑦socket:IP+端口,就是按着TCP SERVE:socket->bind->listen->accept->recv->close。 TCP CLIENT:socket->connect->send->close。来用就可以。
①不同于内核线程,内核不知道应用层线程的存在。
②主线程退出,子线程都会退出。
③一个线程阻塞,会导致整个进程(包含其所有线程)都会阻塞。
④应用层的线程基于线程库实现,时间片分配的单元为进程,所以应用层线程执行的时间少。
①pthread_equal 比较线程ID
②pthread_self 获取自身ID
③pthread_create 创建线程
④pthread_exit 退出线程
⑤pthread_join 等待某线程结束再运行
⑥pthread_cancel 取消线程
①互斥量: 同一时间只能有一个线程获取互斥量(访问数据),访问数据前加锁,访问后解锁。
注意避免死锁:
死锁:两次加锁或者是两个线程互相拥有对方想拥有的锁
避免死锁:同一个线程对一个锁只能加一次、所有的锁都以相同的顺序来获取。
②读写锁: 一次只有一个线程能够占有写锁,可多个线程占有读锁。写锁被占用不能再被占有写锁或者读锁,读锁被占用不能被占用写锁,但是可以占用读锁。(读>写),但是读锁被占用,此时被占用写锁时,后面来的读锁请求都会被阻塞,等写锁释放后,后面的读锁请求才能响应,是为了读锁长期占用,导致写锁不能响应。
③条件变量:给多个线程提供一个会和的场合。如果某线程要等待某条件变为真,会很麻烦循环(加锁、检查、解锁、休眠)。 用条件变量一个线程只需等待条件改变后使用pthread_cond_signal或者pthread_cond_broadcast通知、另一个线程while检查条件并且pthread_cond_wait即可。条件变量要和互斥锁一起使用,互斥量对要检测的条件进行保护,条件变量的函数内部有加锁解锁机制。使用条件变量,减少了条件检查和线程进入休眠等待条件改变的时间。
④自旋锁:不是通过休眠是线程阻塞,而是在线程获取锁前一直忙等待。(cpu不能做其他事情)。自旋锁是一种低开销的锁,只适用于被持有时间段,不希望在调度上花时间的场景。(使用自旋锁,可以避免至少两次上下文切换)
⑤屏障:协调多个线程并行工作的同步机制,使每个线程等待,直到等待的线程数量达到一定的数量,然后所有等待线程一起运行。pthread_join就是一种屏障,允许一个线程等待另一个线程完成。屏障的等待函数pthread_barrier_wait在计数,不满足条件则等待,满足则唤醒所有等待的线程。
⑥信号量:与进程的信号量用法差不多。
⑦信号:与进程的也差不多,只是如何接收不同。
①内核线程切换由内核控制,切换时:用户态->内核态,切换完:内核态->用户态。
②由操作系统内核管理,内核是感知的。
③调度以线程为单位,所以可以很好利用SMP。(应用层的调度以进程为单位)。
④内核线程一般在驱动模块中实现,也可由其他内核线程创建。
①kthread_create:创建内核线程。但是创建了后,不会马上运行,需要用wake_up_process来唤醒。
②kthread_run:创建并运行线程。
③kthread_stop:结束线程。
①原子操作:指令以原子方式执行,不会被打断。但是只能对atomic_t类型进行操作。主要有原子整数运算(32位、64位)、原子位运算。
加锁时间不长并且不会有睡眠----->自旋锁
加锁时间长或者有可能出现睡眠----->信号量
②自旋锁:和之前的应用层的自旋锁差不多,一个线程试图获得已被占用的自旋锁,该线程会忙等待。
不是睡眠,所以可以用于中断上下文(带有睡眠的同步方式都不能用于中断上下文)。
使用自旋锁的临界代码执行的时候,不允许抢占和睡眠,否则,极易发生死锁(同一CPU的线程两次占用自旋锁)。
自旋锁不应该被长时间占有,适用短时间加锁。
自旋锁的实现:
(a)单CPU,不可抢占---->空操作
(b)单CPU,可抢占---->禁止抢占,未实现自旋
(c)多CPU,可抢占---->禁止抢占+自旋
③读写自旋锁:与应用层的读写锁差不多,但这里是基于自旋的,(读>写)。
④信号量:与应用层信号量用法差不多,一种睡眠锁,一个任务试图获得一个不可用的信号量时,信号量会将其推进一个队列,然后让其睡眠,cpu去执行其他任务。
信号量不能在中断上下文中使用,因为信号量可能会导致睡眠
并且使用信号量的时候不允许占有任何自旋锁,因为自旋锁不允许睡眠。
获取信号量时,down_interruptible和down是不同的。使用down_interruptible后,变为TASK_INTERRUPTIBLE状态,可被信号打断;使用down,变为TASK_UNINTERRUPTIBLE,不能被信号打断。
⑤读写信号量:与读写自旋锁差不多,只是这个是基于信号量,会睡眠。
downgrade_write可以把占有的写锁动态变为读锁。
⑥互斥体 mutex:简化版的二值信号量,实现互斥的特定睡眠锁。
上锁和解锁必须在同一上下文,也不能在中断上下文使用,除非mutex约束到使用者,否则优先使用mutex
⑦完成变量:一个任务发出信号通知另一个任务发生了某事件,同步两个任务,代替信号量的简单方法。适用于一个任务等待另一个任务完成某事,感觉就是初值为0的信号量,完成+1,和获取-1。
⑧大内核锁 BLK:以前使用的一个全局自旋锁(现在基本不用了),任务持有BLK锁,睡眠是安全的,可以多次获取一个锁,但只能用于进程上下文。占用BLK锁时睡眠会悄无声息的释放掉锁,重新调度则获取。如果要保护数据,就不能睡眠。
⑨顺序锁:用于读写共享数据(依靠序列计数器)
写入时:获取一个锁,序列值增加。
读取时:读取前读取后各取依次序列值,如果相同则表明读取没有被打断,数据有效;否则数据无效,需要重新读取。写>读,主要无其他写者,可以轻松写值,读者不影响写者。jiffies就使用了顺序锁。
①o 屏障:与应用层的屏障不同,他是为了保证代码的执行顺序,保证代码不会被优化而被打乱顺序。(编译器可能会优化代码从而打乱代码的顺序)。