
Process),顾名思义就是正在执行的应用程序,是软件的执行副本。Light Weighted Process),是 程序执行的基本单位。Linux 的 PThread API 就是用户级线程,KThread API 则是内核级线程。


(线程)运行的过程,会经历以下 3 个状态:




描述信息:这部分是描述进程的唯一识别号,也就是 PID,包括进程的名称、所属的用户等。
资源信息:这部分用于记录进程拥有的资源,比如进程和虚拟内存如何映射、拥有哪些文件、在使 用哪些 I/O 设备等,当然 I/O 设备也是文件。
内存布局:操作系统也约定了进程如何使用内存。如下图所示,描述了一个进程大致内存分成几个 区域,以及每个区域用来做什么。 每个区域我们叫作一个段。

操作系统还需要一张表来管理线程,这就是线程表。线程也需要 ID, 可以叫作 ThreadID。然后线程需 要记录自己的执行状态(阻塞、运行、就绪)、优先级、程序计数器以及所有寄存器的值等等。线程需 要记录程序计数器和寄存器的值,是因为多个线程需要共用一个 CPU,线程经常会来回切换,因此需要 在内存中保存寄存器和 PC 指针的值。创建进程开销大、成本高;创建线程开销小,成本低





fork 会多创造一个克隆的进程,这个克隆的进程,所有状态都和原来的进程一样,但是 会有自己的地址空间。如果要创造 2 个克隆进程,就要 fork 两次。

i++这段程序访问了共享资源,也就是变量i,这种访问共享资源的程序片段我们称为临界区。在临界 区,程序片段会访问共享资源,造成竞争条件,也就是共享资源的值最终取决于程序执行的时序,因此 这个值不是确定的。ThreadLocal、 cas 指令以及 乐观锁。
CPU 的指令,让i++成为一个原子操作。 这个指令的作用是更新一个内存地址的值,比如把i更新为i+1,但是这个指令明确要求使用者必须确定知道内存地址中的值是多少。比如一个线程想把i从100更新到101,线程必须明确地知道现在 i是 100,否则就会更新失败。 但是使用Cas指令 上面的程序还是会错

cas(&oldValue, expectedValue, targetValue) &符号代表这里取内存地址。while(!cas (&i, i, i+1 ) ) { ; } 如果 cas 返回 false,那么会尝试再读一次 i 的值,直到 cas 成功Test-And-Set 指令(tas)。tas 指令的目标是设置一个内存地址的值为 1,它的工作原理和 cas 相似。首先比较内存地址的数据和 1 的值,如果内存地址是 0,那么把这个地址置 1。如果是 1,那么失败。
所以你可以把 tas 看作一个特殊版的cas,可以这样来理解:
tas (&lock) {
return cas(&lock, 0, 1)
}
锁(lock),目标是实现抢占(preempt)。就是只让给定数量的线程进入临界区。锁可以用tas或者 cas来实现。
如果希望同时只能有一个线程执行i++,伪代码可以这么写:
enter();i++; leave();
cas实现enter和leave函数
int lock = 0;
enter(){
while( !cas(&lock, 0, 1) ) { //1:有线程进入临界区
; //线程就会一直执行cas操作,直到锁被释放。
}
}
leave() {
lock = 0;// 没有线程进入临界区
}
//实现让用户传递一个变量过去
int lock = 0;
enter(&lock);
//临界区代码
eave(&lock);
Context Switch,也就是线程切换,因为线程切换比较消耗时间。CPU 资源。如果自旋锁一直拿不到锁,会一直执行。enter() {
while( !cas(&lock, 0, 1) ) {
// 什么也不做
}
}
enter() {
while( !cas(&lock, 0, 1) ) { //cas 失败后
// sleep(1000ms);
wait(); // 等待一个信号——直到另一个线程调用notify方法,通知这个线程结 束休眠。
}
}
竞争条件的版本:
up(&lock){
while(!cas(&lock, lock, lock+1)) { }
}
down(&lock) {
while(!cas(&lock, lock, lock - 1) || lock == 0){}
}
//up和down。up将lock增 1,down将lock减 1。
int lock = 2;
down(&lock);
// 临界区
up(&lock);
//lock等于 1,第 2 个线程还可以进入。
int empty = N; // 当前空位置数量
int mutex = 1; // 锁
int full = 0; // 当前的等待的线程数
wait(){ down(&empty); down(&mutex); insert(); up(&mutex); up(&full);} // 生产者
notify(){ down(&full); down(&mutex); remove(); up(&mutex); up(&empty)}// 消费者
insert(){
wait_queue.add(currentThread);
yield(); //交出当前线程的控制权,当前线程休眠
}
remove(){
thread = wait_queue.dequeue();
thread.resume(); // 恢复
}


int lock1 = 0;
int lock2 = 0;
// 线程1
enter(&lock1); enter(&lock2); leave(&lock1); leave(&lock2);
// 线程2
enter(&lock2); enter(&lock1); leave(&lock1); leave(&lock2)
//上面的程序,如果是按照下面这个顺序执行,就会死锁:
线程1: enter(&lock1);
线程2: enter(&lock2);
线程1: enter(&lock2)
线程2: enter(&lock1) // 死锁发生,线程1、2陷入等待
上面程序线程 1 获得了lock1,线程 2 获得了lock2。接下来线程 1 尝试获得lock2,线程 2 尝试获 得lock1,于是两个线程都陷入了等待。这个等待永远都不会结束,我们称之为死锁
同步的一种方式,就是让临界区互斥。 这种方式,每次只有一个线程可以进入临界区.
+悲观锁: 让临界区互斥的方法(对临界区上锁),具有强烈 的排他性,对修改持保守态度,我们称为悲观锁(Pressimistic Lock) ,通常的上锁就是指悲观锁。MySQL 的表锁、行锁、Java 的锁,本质是互斥 (mutex)。
- 乐观锁: 与悲观锁相反,基于 乐观锁的应用就是版本控制工具 Git,Git 允许大家一起编辑,将结果先存在本地,然后都可以向远程仓 库提交,如果没有版本冲突,就可以提交上去。这就是一种典型的乐观锁的场景,或者称为基于版本控 制的场景。





假设全球有几十亿人都在下单。那么每次下单,需要创建新的一个 Block。这种情况,会导致最后面的 Block,开很多分支。


每处理一个称为一个作业
(Job)。处 理作业最容易想到的就是先到先服务(First Come First Service,FCFS),也就是先到的作业先被计 算,后到的作业,排队进行。用到一个叫作队列的数据结构,具有先入先出(First In First Out,FIFO)性质。先进入队列 的作业,先处理,因此从公平性来说,这个算法非常朴素。另外,一个作业完全完成才会进入下一个作 业,作业之间不会发生切换,从吞吐量上说,是最优的——因为没有额外开销
从另外一个角度来审视短作业优先 的优势,就是平均等待时间:平均等待时间 = 总等待时间/任务数
优先级队列的一种实现方法就是用到了堆(Heap)这种数据结构,更最简单的实现方法,就是每次扫描 一遍整个队列找到优先级最高的任务。也就是说,堆(Heap)可以帮助你在 O(1) 的时间复杂度内查找到最大优先级的元素。
例如应急的任务可以赋予一个更高的优先级,普通任务可以在等待时间(W) 和预估执行时间 (P)中,找一个数学关系来描述。优先级 = W/P
抢占就是把执行能力分时,分成时间片段。 让每个任务都执行一个时间片段。如果在时间片段内,任务完成,那么就调度下一个任务。如果任务没有执行完成,则中断任务,让任务重新排队,调度下一个任务。
抢占结合优先级队列能力,这就构成了一个基本的线程调度模型。线程相对于操作系统是排队到来的,操作系统为每个到来的线程分配一个优先级,然后把它们放入一个优先级队列中,优先级最高的线程下一个执行。

颜色代表被调度线程的时间片段。调度程序可以考虑实现为一个单线程模型,这样不需要考虑竞争条件。
缺点 : 如果一个线程优先级非常高,其实没必要再抢占。无法处理最短优先的抢占。
多级队列,就是多个队列执行调度


n 层,一层层把大任务筛选出来。 最长的任务,放到最闲的时间去执行。要知道, 大部分时间 CPU 不是满负荷的。圆桌上有 5 份意大利面和 5 份叉子。哲学家比较 笨,他们必须拿到左手和右手的 2 个叉子才能吃面。哲学不饿的时候就在思考,饿了就去吃面,吃面的 必须前提是拿到 2 个叉子,吃完面哲学家就去思考。

enum PHIS { THINKING, HUNGRY, EATING } // 枚举 状态
void think() throws InterruptedException { // 思考
System.out.println(String.format("Philosopher %d thinking...", id));
Thread.sleep((long) Math.floor(Math.random()*1000));
this.state = PHIS.HUNGRY;
}
void eat() throws InterruptedException {
synchronized (forks) { // eat方法依赖于forks对象的锁,相当于eat方法这里会同步——因为这里有读取临界区操作做。
if(forks[LEFT(id)] == id && forks[id] == id) { this.state = PHIS.EATING;
} else {
return;
}
} Thread.sleep((long) Math.floor(Math.random()*1000)); // Thread.sleep依然用于描述eat方法的时间开销
}
synchronized 关键字 : 加锁
sleep方法没有放到synchronized内是因为在并发控制时,应该尽量较少锁的范围,这样可以增加更大的并发量。

//每个哲学家用一个while循环表示,
while(true){
think();
_take(LEFT(id));
_take(id);
eat();
_put(LEFT(id));
_put(id);
}
void _take(id){
while(forks[id] != -1) {
Thread.yield();
}
Thread.sleep(10); // 模拟I/O用时
}
_take可以考虑阻塞,直到哲学家得到叉子。上面程序我们还没有进行并发控制,会发生竞争条件。



Deadlock),这是一种饥饿(Starvation)**的形式。从概念上说,死锁是线程间互相等待资源,但是没有一个线程可以进行下一步操作。饥饿就是因为某种原因导致线程得不到需要的资源,无法继续工作。死锁是饥饿的一种形式,因为循环等待无法得到资源。哲学家就餐问题,会形成一种环状的死锁(循环依赖).tryLock的方法是拿不到锁,就报异常,并可以依据这个能力开发释放已获得资源的能力。(LiveLock)的问题。LiveLock 也是一种饥饿。可能在某个时刻,所有哲学及都拿起了左手的叉子,然后发现右手的叉子拿不到,就放下了左手的叉子——如此周而复 始,这就是一种活锁。所有线程都在工作,但是没有线程能够进一步——解决问题。//我们用synchronized构造了一种排队的逻辑。而 哲学家,每次必须拿起所有的叉子,吃完,/
//再到下一哲学家。 这样并发度是 1,同时最多有一个线程在 执行。
//这样的方式可以完成任务,但是性能太差。
while(true) {
synchronized(someLock) {
think();
_take(LEFT(id));
_take(id);
eat();
_put(LEFT(id));
_put(id);
}
}
// 同时拿起,放下过程也同时放下
while(true){ think(); synchronized(someLock) { _takeForks();
} eat(); synchronized(someLock) { _puts();
}
}
void _takeForks(){ if( forks[LEFT(id)] == -1 && forks[id] == -1 ) { forks[LEFT(id)] = id; forks[id] = id;
}
}
void _puts(){ if(forks[LEFT(id)] == id) forks[LEFT(id)] = -1;
if(forks[id] == id) forks[id] = -1;
}
//think函数没有并发控制,一个哲学家要么拿起两个叉子,要么不拿起,这样并发度最 高为 2(最多有两个线程同时执行)。
//而且,这个算法中只有一个锁,因此不存在死锁和饥饿问题。
// 优化 直接把叉子转让给另一个哲学家
void _transfer(int fork, int philosopher) {
forks[fork] = philosopher; dirty[fork] = false;
}
//只要对方不在EATING,就可以考虑转让叉子。
//最后是对 LiveLock 的思考,为了避免叉子在两个哲学家之间来回转让,我们为每个叉子增加了一个 dirty属性。
进程间通信(Intermediate Process Communication,IPC)。所谓通信就是交换数据。
进程1 | 进程2 | 进程3 | mysql -u... -p | 爬虫进程
用管道构建一个微型 爬虫然后把结果入库 达到用shell执行MySQL语句

内存共享不太好用,因此本地消息有两种常见的方法 远程调用 和 消息队列

1.客户端调用函数(方法); 2. stub 将函数调用封装为请求; 3. 客户端 socket 发送请求,服务端 socket 接收请求; 4. 服务端 stub 处理请求,将请求还原为函数调用;5. 执行服务端方法; 6. 返回结果传给 stub; 7. stub 将返回结果封装为返回数据; 8. 服务端 socket 发送返回数据,客户端 socket 接收返回数据; 9. 客户端 socket 将数据传递给客户端 stub; 10. 客户端 stub 把返回数据转义成函数返回值。
RPC 调用的方式比较适合微服务环境的开发,当然 RPC 通常需要专业团队的框架以支持高并发、低延迟 的场景。


CPU 忙碌有 3 种情况: 1.
执行用户空间程序; 2. 执行内核空间程序; 3. 执行中断程序。
CPU 空闲有 2 种情况。 1.CPU 无事可做,执行空闲指令(注意,不能让 CPU 停止工作,而是执行能耗更低的空闲指令)。 2. CPU 因为需要等待 I/O 而空闲,比如在等待磁盘回传数据的中断,这种我们称为 I/O Wait。
top指令 查看当前机器状态的快照
top 看的是一个平均情况,如果想看所有 CPU 的情况可以 top 之后,按一下1键
load average——平均负载. 如果这个值大于1,说明CPU 相当忙碌。因此如果你想发现问题,可以先检查这个指标。

网络状况 : 查看/proc/net/dev
Interface(网络接口),可以理解成网卡
Receive:接收的数据
Transmit:发送的数据
byte 是字节数
package 是封包数
erros 是错误数
drop 是主动丢弃的封包,比如说时间窗口超时了
fifo: FIFO 缓冲区错误
frame: 底层网络发生了帧错误,代表数据出错了
io问题查看命令 :iotop 没有的话 yum install iotop -y
磁盘空间查看可以用df指令

因此在实际工作中,开的线程、进程数往往是超过 CPU 核数的。具体需要 然后进行模拟真实线上压力的测试,分析压测的结果。



Linux 中创建一个进程自然会创建一个线程,也就是主线程。创建进程需要为进程划分出一块 完整的内存空间,有大量的初始化操作,比如要把内存分段(堆栈、正文区等)。创建线程则简单得 多,只需要确定 PC 指针和寄存器的值,并且给线程分配一个栈用于执行程序,同一个进程的多个线程 间可以复用堆栈。因此,创建进程比创建线程慢,而且进程的内存开销更大。
fork() fork() fork() print("Hello World\n") 请问这个程序执行后, 输出结果 Hello World 会被打印几次?print("Hello World\n") 请问这个程序执行后, 输出结果 Hello World 会被打印几次?



在同一时刻,多线程之间,对内存中某个地址的数据认知是否一致(简单理解,就是多个线程读取同一个内存地址能不能读到一致的值)。
对某个地址,和任意时刻,如果所有线程读取值,得到的结果都一样,是一种强一致性,我们称为线性 一致性(Sequencial Consistency),含义就是所有线程对这个地址中数据的历史达成了一致,历史没 有分差,有一条大家都能认可的主线,因此称为线性一致。 如果只有部分时刻所有线程的理解是一致 的,那么称为弱一致性(Weak Consistency)。
int lock = 0
void lock() {
while(!cas(&lock, 0, 1)){
// CPU降低功耗的指令
}
}
上述程序构成了一个简单的自旋锁(spin-lock)。如果考虑到内存一致性模型,线程 1 通过 cas 操作将 lock 从 0 置 1。这个操作会先发生在线程所在 CPU 的 L1 缓存中。cas 函数通过底层 CPU 指令保证了原 子性,cas 执行完成之前,线程 2 的 cas 无法执行。当线程 1 开始临界区的时候,假设这个时候线程 2 开始执行,尝试获取锁。如果这个过程切换非常短暂,线程 2 可能会从内存中读取 lock 的值(而这个值 可能还没有写入,还在 Thread 所在 CPU 的 L1、L2 中),线程 2 可能也会通过 cas 拿到锁。两个线程 同时进入了临界区,造成竞争条件。这个时候,就需要强制让线程 2的读取指令一定等到写入指令彻底完成之后再执行,避免使用 CPU 缓存。
【解析】 基于乐观锁的版本控制、区块链技术。 (非OS ) 处理并发还可以考虑 Lock-Free数据结构。比如 Lock-Free 队列,是基于 cas 指令实现的,允许多个线程使用这个队列。再比如 ThreadLocal,让每个线程访问不同的资源,旨在用空间换时间,也是避免锁的一种方案。
focus 在进步上。解析: 非抢占的先到先服务的模型是最朴素的,公平性和吞吐量可以保证。但是因为希望减少用户的平均等待时间,操作系统往往需要实现抢占。操作系统实现抢占,仍然希望有优先级,希望有最短任务优先。但是这里有个困难,操作系统无法预判每个任务的预估执行时间,就需要使用分级队列。最高优先级的任务可以考虑非抢占的优先级队列。 其他任务放到分级队列模型中执行,从最高优先级时间片段最小向 最低优先级时间片段最大逐渐沉淀。这样就同时保证了小任务先行和高优任务最先执行。
【解析】 线程需要资源没有拿到,无法进行下一步,就是饥饿。死锁
(Deadlock)和活锁(Livelock)都是饥饿的一种形式。 非抢占的系统中,互斥的资源获取,形成循环依赖就会产生死锁。死锁发生后, 如果利用抢占解决,导致资源频繁被转让,有一定概率触发活锁。死锁、活锁,都可以通过设计并发控 制算法解决,比如哲学家就餐问题。
哲学家就餐问题最多允许两组哲学家就餐,如果开销集中在计算上,那么只要同时有两组哲学 家可以进入临界区即可。不考虑 I/O 成本,问题就很简化了,也失去了讨论的意义。比如简单要求哲学 家们同时拿起左右手的叉子的做法就可以达到 2 组哲学家同时进餐。
kill -s USR1 9999 进程 9999 可以通过写程序接收这个信号。 上述过程除了用kill指令外,还可以调用操作系统 API 完 成。sudo badblocks -v /dev/sda5 检查坏道df命令查看自己的文件系统