进程和线程是什么,他们之间如何通信?
进程是程序的一次执行过程,它是操作系统进行资源分配和调度的一个基本单位。
线程是进程中的一个执行单元,是CPU调度和分派的基本单位。一个进程至少包含一个线程,通常称为主线程。一个进程中可以包含一个或多个线程。
进程间通信(IPC):
- 管道(Pipes)
管道是一种半双工的通信方式,数据只能单向流动。通常用于具有亲缘关系的进程之间的通信,如父进程和子进程。
- 消息队列(Message Queues)
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享内存(Shared Memory)
共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与信号量联用,用来实现进程间的同步和通信。
- 套接字(Sockets)
套接字也是一种进程间通信机制,主要用于不同机器上的进程通信,但也可以用于同一台机器上的进程通信
- 信号(Signals)
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
- 信号量(Semaphores)
信号量是一个计数器,可以用来控制多个进程对资源的访问。它常用于解决进程间的同步问题。
线程间通信(IPC):
- 共享内存
线程可以直接访问同一进程内的全局变量或静态变量,但由于线程共享同一段内存,因此需要通过互斥锁(Mutex)或其他同步机制来保证数据的一致性。
- 条件变量(Condition Variables)
条件变量可以配合互斥锁使用,实现线程间的同步。条件变量可以让一个或多个线程在一个或多个共享变量达到某个条件前一直等待。
- 互斥锁(Mutexes)
互斥锁用于保护共享资源不受并发访问的影响,确保在某一时刻只有一个线程可以访问共享资源。
- 原子操作(Atomic Operations)
原子操作是一些内置的函数调用或宏,用于保证某些关键代码块的执行不会被打断。
- 生产者-消费者模型
生产者-消费者模型是通过队列实现的一种经典的线程间通信模式,其中生产者负责向队列中添加元素,消费者负责从队列中取出元素。
网络通信建立连接为什么是三握手?
三次握手的具体步骤
客户端发送 SYN 包:
客户端向服务器发送一个 SYN 数据包,其中包含客户端的初始序列号 ISN,并且 SYN 标志位置为 1。
SYN 数据包的序列号 ISN 用于标识后续数据的第一个字节的位置。
服务器响应 SYN-ACK 包:
服务器接收到 SYN 包后,向客户端发送一个 SYN-ACK 数据包,其中包含服务器的初始序列号 ISN,并且 SYN 标志位置为 1,ACK 标志位置为 1。
SYN-ACK 数据包的序列号 ISN 用于标识后续数据的第一个字节的位置,ACK 字段设置为客户端序列号加一的值(ACK=ISN+1)。
客户端发送 ACK 包:
客户端接收到 SYN-ACK 包后,向服务器发送一个 ACK 数据包,其中 ACK 标志位置为 1。
ACK 数据包的 ACK 字段设置为服务器序列号加一的值(ACK=ISN+1)。
两次握手的问题:
连接确认缺失:即使服务器收到了客户端的 SYN 包并回复了 SYN-ACK 包,但如果客户端没有收到这个 SYN-ACK 包,它也不会知道连接是否建立成功。
资源浪费:如果客户端没有收到服务器的 SYN-ACK 包,它不会继续后续的数据传输,但服务器却可能已经为这个连接分配了资源,导致资源浪费。
已失效的连接请求:两次握手同样无法防止旧的连接请求(SYN 包)突然到达服务器的情况。如果旧的 SYN 包到达服务器,服务器可能会误认为这是一个新的连接请求并尝试建立连接,从而造成资源浪费。
算法,阶梯问题,每次只能走一步或者两步,N步的阶梯共有多少种走法?
算法理论:第N步的步数,可以由**第N-1步的走法数+ 1步 and 第N-2步的走法数+ 2步 **合计得到推导公式
public static void main(String[] args) throws InterruptedException {
// customThreadPool();
// 算法,阶梯问题,每次只能走一步或者两步,N步的阶梯共有多少种走法
// f1 = 1; 1
// f2 = 2; 11 2
// f3 = 3; 111 21 12
// f4 = 5; 1111 121 211 112 22
// f5 = 8; 11111 1211 2111 1121 221 1112 212 122
// 公式推导:f(n) = f(n-1) + f(n-2)
long start = System.currentTimeMillis();
int res;
if (N < 0) res = 0;
if (N == 1 || N == 2) res = N;
res = calculate(N);
System.out.println(N + "步的阶梯共有" + res + "种走法");
System.out.println("cost:" + (System.currentTimeMillis() - start) + "ms");
long start2 = System.currentTimeMillis();
Map<Integer, Integer> map = new HashMap<>(N);
map.put(1, 1);
map.put(2, 2);
for (int i = 3; i <= N; i++) {
map.put(i, map.get(i - 1) + map.get(i - 2));
}
System.out.println(N + "步的阶梯共有" + map.get(N) + "种走法");
System.out.println("cost:" + (System.currentTimeMillis() - start2) + "ms");
}
private static int calculate(int n) {
if (n < 0) return 0;
if (n == 1 || n == 2) return n;
return calculate(n - 1) + calculate(n - 2);
}
IO模型
为了避免用户进程直接操作内核,保证内核安全,操作系统将内存(虚拟内存)划分为内核空间和用户空间两部分。内核缓冲区(在系统内存中)和进程缓冲区(用户内存中),操作系统内核只有一个内核缓冲区,而每个用户程序都有自己独立的缓冲区(即进程缓冲区),设置缓冲区的目的为:减少频繁地与设备之间的物理交换。
- 阻塞IO模型:在应用调用read()函数读取数据时,其系统调用直到数据包到达且被复制到应用缓冲区或者发送错误时才返回,在此期间会一直等待,进程从调用到返回这段时间内都是被阻塞的,称为阻塞IO。
- 非阻塞IO模型:在应用发起读取申请时,若内核数据没准备好会立刻返回错误码,此时可以轮询和执行其他任务。
- IO多路复用模型:IO复用模型的思路就是系统提供了一种函数可以同时监控多个fd的操作,这个函数就是我们常说到的select、poll、epoll函数,有了这个函数后,应用线程通过调用select函数就可以同时监控多个fd,select函数监控的fd中只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时询问线程再去通知处理数据的线程,对应线程此时再发起recvfrom请求去读取数据。
- select函数仅仅知道有几个IO事件发生了,但并不知道具体是哪几个socket连接有IO事件,还需要轮询去查找,时间复杂度为
,处理的请求越多,所消耗的时间越长。单个进程所打开的fd是有限制的,通过FD_SIZE设置,默认为1024;
- poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,但是poll没有最大连接数的限制,原因是它是基于链表来存储的。
- epoll,linux内核中存在,可以理解为event pool,不同于select、poll的轮询机制,epoll采用的是事件驱动机制,每个fd上注册有回调函数,当网卡接收到数据时会回调该函数,同时将该fd的引用放入rdlist就绪列表中。当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
- select函数仅仅知道有几个IO事件发生了,但并不知道具体是哪几个socket连接有IO事件,还需要轮询去查找,时间复杂度为
- NIO模型:new IO,NIO 是 Java 平台的一部分,包含channels,buffers,selectors,channels支持非阻塞,可以同时执行其他任务,buffers可以减少用户空间和内核空间的拷贝次数提高性能,selectors负责监控channels的状态变化,适合处理大量的并发连接
- 异步IO模型:AIO 允许应用程序在发起一个 I/O 请求后继续执行其他任务,而操作系统在 I/O 操作完成后会通过回调函数或事件通知应用程序。AIO 适用于需要处理大量并发 I/O 操作的场景,尤其是当 I/O 操作涉及磁盘访问时。epoll更适用于网络 I/O 场景,而 AIO 更适用于磁盘 I/O 场景。