前言:秋招就要开始啦!!!能不能找到一个满意的职位就看它啦!
这个我准备了很长时间,是一些对我自己来说觉得薄弱,掌握不牢的一些知识点的总结,一些自己掌握的还行的知识点就没有记录下来,所以可能不是很全,但是内容也算相当丰富啦!!!希望对看到的小伙伴有所帮助!!!
同步更新于个人博客系统:python后端面试笔记,祝愿秋招拿到满意的offer。
进程是计算机资源分配的最小单元。
线程是CPU调度的最小单元。
一个进程中可以有多个线程,同一个进程中的线程可以共享此进程的所有资源。
进程与进程之间是相互隔离的。
import multiprocessing
multiprocessing.set_start_method("spawn")
方法 | 子进程和主进程之间资源的关系 | 操作系统 | |
---|---|---|---|
fork | 会在子进程中拷贝几乎所有主进程的资源 | unix | 支持文件对象/线程锁等传参 |
spawn | run参数传必备资源 | unix,win | 不支持文件对象/线程锁等传参 |
forkserver | run参数传必备资源 | 部分unix | 不支持文件对象/线程等传参 |
import multiprocessing
#要想定义自己的进程类,只用继承自multiprocessing.Process,并重写run()方法即可
class MyProcess(multiprocessing.Process):
#重写run()方法
def run(self) -> None:
print("hello world")
if __name__ == '__main__':
my_process = MyProcess()
my_process.start()
#获取cpu的核数
print(multiprocessing.cpu_count())
import multiprocessing
from time import sleep
def func(child_conn):
sleep(1)
child_conn.send('hello world')
child_conn.send('hello world2')
info = child_conn.recv() # 阻塞两秒钟
print(info)
if __name__ == '__main__':
parent_conn, child_conn = multiprocessing.Pipe() # recv之前必须有一个send,否则一直处于阻塞状态
p = multiprocessing.Process(target=func, args=(child_conn,))
p.start()
# p.join()
info = parent_conn.recv() # 阻塞一秒钟
print(info) # hello world
sleep(2)
parent_conn.send("bye bye")
GIL是全局解释器锁,是CPython解释器特有的一个玩意,其作用就是:让一个进程中同一个时刻只能有一个线程可以被CPU调度。
常见的程序开发中,计算操作需要使用CPU多核优势,IO操作不需要使用CPU多核优势,所以:
join()的作用是让当前线程执行完成后再继续向下执行,此时程序处于阻塞状态。
th.setDaemon(True) #守护线程
线程守护,主线程执行完毕后,子线程也自动关闭
非守护线程:主线程等待子线程,子线程执行完毕后,主线程才结束。(默认)
一个进程中可以有多个线程,且线程共享进程中所有的资源。多个线程去同时操作一个“东西”时,可能会存在数据混乱的情况。
python中线程安全的数据结构:
在线程中如果想要自己手动加锁,一般有两种:Lock和RLock。(threading.Lock,threading.RLock)
由于竞争资源或者由于彼此通信而造成的一种阻塞现象。
python3中才正式提供线程池。
线程不是开的越多越好,开多了可能反而导致系统的性能降低。
from concurrent.futures import ThreadPoolExecutor
from time import sleep
#任务
def task(num):
sleep(1)
print(f'我的编号是:{num}')
#创建一个线程池,最多维护10个线程
pool=ThreadPoolExecutor(10)
for i in range(1000):
#在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行;
#执行完毕后再将线程还给线程池;
#如果没有空闲线程,则等待
pool.submit(task,i)
pool.shutdown(True) #等待所有线程执行完成后才往下走(类似于线程中的join()方法)
print('END')
from concurrent.futures import ThreadPoolExecutor
from time import sleep
# 任务
def task(num):
sleep(1)
print(f'我的编号是:{num}')
return f'{num}'
# 某一个任务完成后需要执行的任务
# 函数的参数response,调用response.result()方法可以获得该线程的执行的函数的返回值,若无返回值,则返回None
def done(response):
'''
这样写的好处是,可以将任务更加原子化。
例如:在发送请求下载一张图片时,可以将发送请求获得返回值封装成一个任务
将数据写入文件封装成一个任务。
'''
print(response.result())
print(type(response.result()))
# 创建一个线程池,最多维护10个线程
pool = ThreadPoolExecutor(10)
for i in range(1000):
# 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行;
# 执行完毕后再将线程还给线程池;
# 如果没有空闲线程,则等待
future = pool.submit(task, i)
future.add_done_callback(done) #########
pool.shutdown(True) # 等待所有线程执行完成后才往下走(类似于线程中的join()方法)
print('END')
from concurrent.futures import ProcessPoolExecutor
from multiprocessing import cpu_count
# 子进程
def return_str(num):
return f'hello world{num}'
# 子进程结束后的回调函数
def print_str(response):
print(response.result())
if __name__ == '__main__':
p_pool = ProcessPoolExecutor(cpu_count()) # 创建和自己电脑cpu核数相同进程数的进程池
for i in range(100):
# 给进程池添加任务
fur = p_pool.submit(return_str, i)
fur.add_done_callback(print_str) # 添加回调函数
# 让主线程等待子线程执行完再往下走
p_pool.shutdown()
print("END")
协程也可以被称为微线程,是一种用户态内的上下文切换技术,在开发中结合遇到IO自动切换,就可以通过一个线程实现并发操作。
个人理解:在同一个线程中,有很多任务,为了提高执行效率,可以让这些任务同时执行(宏观上并发,微观上是串行),但是在某一个具体的任务中会遇到IO操作(或其他可能引起阻塞的操作),所以可能会有短暂的阻塞,此时就可以跳到另一个任务去执行,这样就保证了CPU一直处于被调度的状态,高效地利用了资源。
通常情况下,每次实例化一个新对象时,都会为对象开辟新的空间,这样在一定程度上拉低了程序的效率。而单例模式就是用来解决这个问题的,在单例模式中,同一个类下所有的对象都共用同一个地址。
python实现单例模式:
class Singleton:
instance = None
def __new__(cls, *args, **kwargs):
# 在创建一个新的对象时,首先判断有没有已经存在的对象
# 如果没有存在的对象就创建一个新的对象
# 这样就保证了所有的对象使用的都是同一个地址
if not cls.instance:
cls.instance = object.__new__(cls)
return cls.instance
if __name__ == '__main__':
obj1 = Singleton()
obj2 = Singleton()
print(obj1)
print(obj1)
print(obj1 == obj2)
闭包指延伸了作用域的函数,其中包含函数定义体中引用,同时包含不在定义体中定义的非全局变量。
关键是它能访问定义体之外定义的非全局变量!!!
# 闭包指延伸了作用域的函数,其中包含函数定义体中引用,同时包含不在定义体中定义的非全局变量。
# 关键是它能访问定义体之外定义的非全局变量!!!
# 利用闭包实现avg(num):计算不断增加系列值的均值
def make_avager():
history = []
def avg(num):
history.append(num)
return sum(history) / len(history)
return avg
if __name__ == '__main__':
avg = make_avager()
print(avg(1))
print(avg(2))
print(avg(3))
print(avg(4))
print(avg(5))
#avg.__closure__中的各个元素对应于avg.__code__.co_freevars中的一个名称
print(avg.__code__.co_freevars)
print(avg.__closure__) #对应于history
print(avg.__closure__[0].cell_contents) #history的值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iNuBEm3s-1660988023945)(C:\Users\LLL03\Desktop\python\8.面试\imgs\1647003515639.png)]
概念:函数修饰器用于在源码中“标记”函数,以某种方式增强函数的行为。
装饰器的典型行为:把被装饰的函数替换成新的函数,二者接受相同的函数,并且返回被装饰的函数本该返回的值,同时还会做一些额外的操作。(在不修改源代码的前提下增加新的功能。)
import time
# 一个简单的装饰器,输出函数的运行时间
def clock(func):
@wraps(func) # wraps装饰器会把__name__,__doc__等属性,从func(被装饰的函数)复制给clocked(实际运行的函数)
def clocked(*args, **kwargs):
t0 = time.perf_counter()
result = func(*args, **kwargs) # 通过*args获取func的参数
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ','.join(str(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked
@clock
def factorial(n):
return 1 if n < 2 else n * factorial(n - 1)
if __name__ == '__main__':
print(factorial(100))
python慢的原因主要有两个,首先我们知道python是强类型动态解释性语言,慢就慢在动态和解释!
由于hash表的大小是有限的,而要存储的数据总是无限的,因此对于任何hash函数,都会出现两个不同的元素映射到同一个位置上,这种情况就叫做hash冲突。
解决方法:开放寻址法,拉链法
如果hash函数返回的位置已经有值,则可以向后探查新的位置来存储这个值。
hash表每一个位置都连接一个链表,当冲突发生时,冲突的元素将被追加到该位置链表的最后。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uI3PydvY-1660988023946)(C:\Users\LLL03\Desktop\python\8.面试\imgs\1658214053698.png)]
对于列表,元组等序列,不管是深拷贝还是浅拷贝,都会创建新的对象,但是,对于浅拷贝,序列里的元素是对原序列元素的引用(地址拷贝),对于深拷贝,序列里的元素是创建的新的对象(值拷贝)。
特例:共享字符串,“热门数字”等等,为了节省空间,一些”常见“的变量会驻留内存,在一次程序执行的过程中,不管是深拷贝还是浅拷贝,它们的地址都不会改变。默认做浅拷贝(如下):
RESTful API是指符合REST风格的web接口。是一种设计风格,而不是标准。
在请求表单时,Django会同时传一个字段给前端,同时将这个字段保存在服务端,前端提交表单给服务端时,服务端会首先验证这个字段是否和服务端的一致,如果是一致的,才会继续后面的业务逻辑。
###引用计数法
在cpython解释器中,提供的是引用计数法来进行垃圾回收的。其实,python中的每一个对象都对应于c中的一个结构体,在创建python对象前,cpython解释器会生成一个循环的双向链表,然后依次将这些python对象加入双向链表中,而这些python对象的结构体中,有一个公共的属性ob_refcnt,就是保存对当前对象的引用数量,初始值为1。
回收规则
循环引用
a = [1, 2, 3] # 创建对象a,a的引用次数为1
b = [4, 5, 6] # 创建对象b,b的引用次数为1
a.append(b) # 将b添加到a中,对b进行了一次引用,b的引用次数加一后为2
b.append(a) # 将a添加到b中,第a进行了一次引用,a的引用次数加一后为2
del a # 删除a变量,a的引用次数减一为1,不为0,不会将a回收,其任然驻留在内存,但此时已无法使用a
del b
可以看到,如果仅仅使用引用计数进行垃圾回收,在碰到循环引用时,一些经删除后的变量已无法使用,但是它们任然没有被回收,一直驻留在内存,造成内存泄漏,大量的内存泄漏必然导致程序最后无法执行。
为了弥补引用计数的不足,出现了标记清除和分代回收来弥补它的不足。
目的:为了解决引用计数法无法解决循环引用导致的内存泄漏的问题而出现的。
实现:在python底层维护一个链表,用来保存那些存在循环引用的对象(list,tuple,set,dict)。一定条件下触发扫描条件,会扫描这个链表,发现如果某个对象是循环引用,就将其对应的引用计数次数减一,引用计数为0后就将其清除。
将可能存在循环引用的对象维护成三个链表:
存储引擎其实就是如何存储数据,如何为存储的数据建立索引和如何更新,查询数据等技术的实现方法。因为在关系数据库中数据的存储是以表的形式存储的,所以存储引擎也可以称为表类型(即存储和操作此表的类型)。在oracle和sql server等数据库中只有一种存储引擎,所有的数据存储管理机制都是一样的。而MySQL数据库提供了多种存储引擎。用户可以根据不同的需求为数据表选择不同的引擎,用户也可以根据自己的需求编写自己的存储引擎。
MySQL支持的存储引擎如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eKmOdLSp-1660988023947)(C:/Users/LLL03/Desktop/python/8.面试/imgs/1646552555967.png)]
innodb和mysiam的区别
角度 | Innodb | mysiam |
---|---|---|
事务 | 支持事务,且是事务安全的,每一条sql语句都默认封装成事务 | 不支持 |
锁 | 支持行级锁和表锁 | 仅支持表锁 |
索引 | 不支持全文索引(5.7已支持) | 支持全文索引 |
适用场景 | 大型安全的应用 | 效率快,跨平台支持,适用于小型应用 |
外键 | 支持 | 不支持 |
如何选择:
MySQL官方对索引的定义:索引是帮助MySQL高效获取数据的数据结构。
当数据量大的时候,索引的数据量也很大,所以索引不可能全部放到内存中,因此索引一般以文件的形式存储到硬盘上。
//创建索引
create [unique] index indexName on tableName(columnName(length));
//注意: 如果是CHAR,VARCHAR类型,length可以小于字段实际长度;如果是BLOB和TEXT类型,必须指定length
//修改索引
alter tableName add [unique] index [indexName] on (columnName(length));
//删除索引
drop index [indexName] on tableName;
//查看索引
show index from tableName;
失效情况
解决方案
让like字段是查询字段,即 select name from table where name like “%张三%”;
使用like “张三%”,即select name,age from table where name like “张三%”;
当查询语句中使用order by进行排序时,如果没有使用索引进行排序,会出现filesort文件内排序,这种情况在数据量大或者并发高的时候,会有性能问题,需要优化
filesort出现情况举例
解决方案
B树和B+树的主要区别在于非叶节点是否存储数据,B树在非叶节点会存储数据,而B+树的所有数据都存在叶节点中;同时,在B+树中,叶节点之间使用双向的指针相连,构成了一个双向的有序链表。
Mysql存储引擎由B树升级到B+树的主要原因是:优化范围查询的性能,在B+树中,当查找到范围内的第一个值后,直接通过叶子节点之间的指针查找后面满足条件的值即可,而B树只能对范围内的每一个值都进行查询。
原子性是指事务是一个不可分割的单位,是最小的操作单元,在一个事务中的所有sql语句,要么全部执行成功,要么全部执行失败,在执行过程中,只要有一条sql语句执行失败,就会执行回滚操作,将数据恢复到执行当前事务之前的状态。能保证原子性的一个重要东西就是undo log,这是一个日志文件,在执行sql语句时,会同时将执行记录写入这个日志文件,如果事务提交失败,就会根据这个undo log日志,执行相反的操作(回滚),比如:如果执行了一个insert操作,就会执行delete操作,如果执行了delete操作就会执行insert操作,如果执行了update操作,就会执行相反的update操作,将值恢复到之前的值。
事务执行之后,数据库的完整性约束没有被破坏,事务执行前后都是一个合法的数据状态。完整性体现在比如数据库的主键要唯一,字段类型大小要符合要求,外键的约束要符合要求。一致性是事务追求的最终目标。原子性、持久性、隔离性都是为了保证数据库最终的一致性。如果另外三个特性无法保证,那么一致性肯定也保证不了 。
**与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。**隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。
事务的隔离级别
SQL标准中定义了四种隔离级别,并规定了每种隔离级别下上述几个问题是否存在。一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差。隔离级别与读问题的关系如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fBHsIhKD-1660988023949)(C:\Users\LLL03\Desktop\python\8.面试\imgs\1174710-20190128201034603-681355962.png)]
持久性是指事务一旦提交,它对数据库的改变是永久性的。
通过redo log实现的。
redo log什么时候写入磁盘?
redo log在同步到磁盘之前,它是在缓存区中,它的更新机制是通过语句innodb_flush_log_at_trx_commit命令来控制,有三个可控选项:
缓存雪崩是指缓存同一时间大面积失效,所以,后面的请求都会落到数据库上面,造成数据库短时间承受大量的请求而崩掉。
解决方案:
缓存穿透是指缓存和数据库中都没有数据,导致所有请求都落在数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
缓存击穿是指缓存中没有数据,但数据库中有(一般是缓存过期造成),这时由于并发用户特别多,同时读缓存没有读到数据,又同时去读数据库,引起数据库压力瞬时增大。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同的数据都过期了,很多数据都查不到从而查数据库。
解决方案:
redis的字典,底层有两个数组,以及一个重要的字段rehashidx,用来控制rehash,默认值是-1,如果其值不为-1,就说明正在rehash这个过程中;默认情况下,数组1保存着元素,数组2为NULL,当元素个数和数组长度一样时,就会发生扩容,此时会为数组2开辟2倍数组1大小的空间,同时rehashidx由默认值变为0(rehashidx!=-1标志着在rehash状态!!!),之后对字典的crud操作,都会进行一次单步的rehash操作,比如:在字典中新增一个元素时,会首先迁移数组1中的一个元素到数组2中,之后再进行新增操作,并且新增的元素会直接添加到数组2中,当数组1中的元素个数为0时,rehash结束,此时rehashidx的值变为-1,标志着rehash结束。
redis中默认的数据库有16个,默认使用dbid=0的数据库。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nrTIn7p5-1660988023949)(C:\Users\LLL03\Desktop\python\8.面试\imgs\1648280203367.png)]
//可以使用select index进行数据库的切换
select 5 //使用dbid=5的数据库
//设置值
set key val
//批量设置
mset k1 v1 k2 v2 k3 v3 ...
//设置值的同时设置过期时间
setex key seconds val
//先判断key是否存在,不存在才能设置(适合用于分布式)
setnx key val
//批量setnx操作
msetnx k1 v1 k2 v2 k3 v3 ... //需要注意的是,这个是原子性的操作,只要其中有一个不能设置成功,所有的都不会成功,即要么一起成功,要么一起失败!
//取值
get key
//批量获取
mget k1 k2 k3 ...
//先get再set
getset key val
//设置对象(设置1号user的属性和值的两种方式,mset效率会更高)
set user_1 {k1:v1,k2:v2,k3:v3...}
mset user_1_k1 v1 user_1_k2 v2 user_1_k3 v3 ...
//获取当前数据库的大小
dbsize
//清空当前数据库
flushdb
//清空所有数据库
flushall
//判断某个键是否存在
exists key
//移除某个kv
move key db
//为某个键kv设置过期时间
expire key seconds
//查看某一个kv还有多长时间过期
ttl key
//查看v的类型
type key
//添加一个或多个元素到列表中(从左边添加)
lpush key val1 val2 val3 ...
//从右边添加
rpush key val1 val2 val3 ...
//范围取值
lrange key start stop
//索引取值(从下标0开始)
lindex key index
//移除值,并返回值(从左边开始移除)
lpop key
//从右边开始移除
rpop key
//获取列表的长度
llen key
//移除指定数量的某个值(count指定数量,val指定值,从列表的左边开始寻找符合要求的值)
lrem key count val
//截断,移除指定索引范围内的元素
ltrim key start stop
//更新值(执行该命令的前提条件:key必须存在,index没有超出范围)
lset key index val
//插入值(在值pivot的前面或者后面插入val,如果找不到pivot会失败,从列表的左边开始找起,找到第一个为止)
linsert key before|after pivot val
//给集合添加成员
sadd key member1 member2 ...
//查看集合中的所有成员
smembers key
//查看集合中成员的个数
scard key
//查看某个成员是否在集合中
sismember key member
//随机获取指定数量的成员(默认获取一个)
srandmember key [count]
//随机移除指定数量的成员(默认是一个)
spop key [count]
//组合命令
//将一个集合中的值移动到另一个集合中
smove source destination member
//差集
sdiff key1 key2 ...
//交集
sinter key1 key2 ...
//并集
sunion key1 key2 ...
//给某一个hash表添加k-v
hset key field val
//同时设置多个值
hmset key field1 val1 field2 val2 ...
//获取值
hget|hmget key field [field2...]
//获取所有值
hgetall key
//删除值
hdel key field1 field2...
//获取hash表的长度
hlen key
//判断某一个字段是否在hash表中
hexists key field
//获取hash表所有的键
hkeys key
//获取hash表所有的值
hvals key
//让hash中某个字段的值加一[减一] increment指定增减数量
hincreby key field increment
例:利用工资对成员进行排序
//添加成员
zadd key score1 member1 score2 member2...
//按照score进行升序排列(min,max指定score的区间)
zrangebyscore key min max [withscores]
//降序排列(start和stop指定起始位置)
zrevrange key start stop [withscores]
//移除成员
zrem key member
//获取集合的大小
zcard key
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CYMbYQtk-1660988023949)(C:\Users\LLL03\Desktop\python\8.面试\imgs\1648956791979.png)]
官方解释: CPU并不是您使用Redis的瓶颈,因为通常Redis要么受内存限制,要么受网络限制。例如,使用在一般Linux系统上运行的流水线Redis每秒可以发送一百万个请求,因此,如果您的应用程序主要使用O(N)或O(log(N))命令,则几乎不会使用过多的CPU 。但是,为了最大程度地利用CPU,您可以在同一服务器上启动多个Redis实例,并将它们视为不同的服务器。在某个时候,单个实例可能还不够,因此,如果您要使用多个CPU,则可以开始考虑更早地分片的某种方法。但是,在Redis 4.0中,我们开始使Redis具有更多线程。目前,这仅限于在后台删除对象,以及阻止通过Redis模块实现的命令。对于将来的版本,计划是使Redis越来越线程化。
既然redis的瓶颈不是cpu,那么在单线程可以实现的情况下,自然就使用单线程了。
tips:
但是,我们使用单线程的方式是无法发挥多核CPU 性能,不过我们可以通过在单机开多个Redis 实例来完善!
这里我们一直在强调的单线程,只是在处理我们的网络请求的时候只有一个线程来处理,一个正式的Redis Server运行的时候肯定是不止一个线程的,这里需要大家明确的注意一下!例如Redis进行持久化的时候会以子进程或者子线程的方式执行 。
排序 | 平均情况 | 最好情况 | 最坏情况 | 稳定与否 | 空间复杂度 |
---|---|---|---|---|---|
冒泡排序 | O(N2) | O(N) | O(N2) | 稳定 | 1 |
选择排序 | O(N2) | O(N2) | O(N2) | 不稳定 | 1 |
插入排序 | O(N2) | O(N) | O(N2) | 稳定 | 1 |
快速排序 | O(NlogN) | O(NlogN) | O(N2) | 不稳定 | O(logN) |
归并排序 | O(NlogN) | O(NlogN) | O(NlogN) | 稳定 | O(N) |
堆排序 | O(NlogN) | O(NlogN) | O(NlogN) | 不稳定 | 1 |
最佳置换算法,其所选择淘汰的页面是以后永不使用的,或者在最长时间内不会被访问的页面。
最佳置换算法是一种理想状态,因为无法预知,所以该算法是无法实现的,采用最佳置换算法可保证获得最低的缺页率。
实用价值:给别的置换算法作参考对比。
FIFO算法是最早出现的置换算法。该算法总是淘汰最新进入内存的页面,即选择在内存中驻留时间最长的页面进行淘汰。
如果一个数据在最近一段时间没有被访问,那么可以认为它在将来被访问的可能性很小,因此,当空间满时,最久没有被访问的就会最新被淘汰。
个人理解:LRU算法是对FIFO算法的一种优化,在FIFO中先进的一定是最久未使用的,但是在LRU中,如果后面进入的页面是命中最先进入的页面时,该页面就会被更新,从而不是“最久未使用的”。
如果一个数据在最近一段时间很少被访问,那么可以认为它在将来被访问的可能性很小,因此,当空间满时,访问频率最小的数据最先被淘汰。
共享内存
在通信的进程之间存在一块可直接访问的共享空间,通过对这片共享空间进行读写操作实现进程之间的信息交换。在对共享空间进行读写操作时,需要使用同步互斥工具,对共享空间的读写进行控制。
消息传递
在消息传递系统中,进程间的数据交换以格式化的消息为单位。若通信的进程间不存在可直接访问的共享空间,则必须利用操作系统提供的消息传递方法实现进程通信。进程通过系统提供的发消息和接受消息两个原语进行数据交换。这种方式隐藏了通信实现的细节,使通信过程对用户透明,简化了通信程序的设计,是当前应用最广泛的进程间通信机制。
管道通信
管道通信是消息传递的一种特殊方式。所谓“管道”,是指通过用于连接一个读进程和一个写进程以实现它们之间的通信的一个共享文件,又名pipe文件。
把应用程序在逻辑上的地址映射到真实的物理内存上,这种逻辑上的内存就是虚拟内存。
基于局部性原理,在程序装入时,仅需要将程序当前要运行的少数页面或段先装入内存,而将其余部分暂留在外存,便可启动程序执行。在程序装入的过程中,当所访问的信息不在内存时,由操作系统将所需要的部分调入内存,然后执行程序。另一方面,操作系统将内存中暂不使用的内存搬到外存上,从而腾出空间放将要调入内存的信息。这样,系统好像为用户提供了一个比实际内存容量大很多的存储器,称为虚拟存储器。
网络协议是分层的,每一层都有各自的作用和职责,我们常见的网络协议有五层,从上到下依次是:
应用层:为用户提供功能
传输层:应用层的数据包会传给传输层,传输层是为应用层提供网络支持的,传输层有两个传输协议:TCP和UDP。
网络层:真正负责传输功能的一层,常用的协议是IP协议
数据链路层:为⽹络层提供链路级别传输的服务
物理层:把数据包转换成电信号,让其可以在物理介质中传输
不管是TCP还是UDP,它们都属于传输层的协议。它们之前的主要区别如下:
TCP(Transmission Control Protocol),传输控制协议 | UDP(User Datagram Protocol),用户数据报协议 | |
---|---|---|
连接性 | 面向连接 | 无连接 |
可靠性 | 可靠传输,不丢包 | 不可靠传输,可能会丢包 |
首部占用空间 | 大 | 小 |
传输速率 | 慢 | 快 |
资源消耗 | 大 | 小 |
应用场景 | 浏览器,文件传输,邮件发送 | 音视屏通话,直播 |
应用层协议 | HTTP,HTTPS,FTP,SMTP,DNS | DNS |
服务对象 | TCP是一对一的两点服务,即一条连接只有两个端点 | UDP支持一对一,一对多,多对多 |
TCP是面向连接的,虽然资源消耗大,但是可靠传输,只支持一对一的两点服务;UDP是无连接的,虽然是不可靠传输,但是资源消耗小,传输效率高,支持一对多,多对多的服务。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cfyAgPTL-1660988023950)(C:\Users\LLL03\AppData\Roaming\Typora\typora-user-images\image-20220819161554539.png)]
TCP三次握手就是建立一个TCP连接。
TCP的连接主要解决以下三个问题:
是TCP双方能够确认对方的存在
协商一些参数(如果最大窗口值,是否使用窗口扩大选项等等)
使TCP双方能够对运输实体资源(如缓存大小,连接表中的数据等)进行分配。
TCP建立连接的过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1qBFlIyP-1660988023950)(C:\Users\LLL03\AppData\Roaming\Typora\typora-user-images\image-20220819151844153.png)]
数据传输结束后,TCP双方都可以释放连接。
假设客户端的应用进程通知其主动关闭TCP连接,则四次挥手过程如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q2RxzZiU-1660988023951)(C:\Users\LLL03\AppData\Roaming\Typora\typora-user-images\image-20220819161431995.png)]
在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络性能就要变坏。这种情况就叫做拥塞。
发送方维护一个叫拥塞窗口cwnd的状态变量,其值取决于网络的拥塞程度,并且动态变化。拥塞窗口cwnd的维护原则:只要网络没有出现拥塞,拥塞窗口就再增大一些;但只要网络出现拥塞,拥塞窗口就减小一些。
发送方将拥塞窗口作为发送窗口swnd,即swnd=cwnd,同时会维护一个慢启动门限ssthresh状态变量:
判断出现网络拥塞的依据:没有按时收到应当达到的确认报文(即发生超时重传)
假设在某次传输过程中,部分报文段丢失,发送方一直接收不到确认,发送方中的超时重传计时器就会发生超时,从而发生重传。
发生超时重传后,发送方以此判断网络发生了拥塞,进而执行以下工作:
慢开始(slow-start):发送方每收到一个对新报文段的确认时,就把拥塞窗口的值扩大一倍,然后开始下一轮的传输,当增大到慢开始门限是就会停止使用慢开始,转而使用拥塞避免。
拥塞避免(congestion avoidance):发送方每收到一个对新报文段的确认时,就把拥塞窗口的值加1,然后开始下一轮的传输
快重传(fast retransmit):就是使发送方尽快发生重传,而不是等超时重传计时器超时再重传。(应对错误的判断为网络拥塞的情况)
快恢复(fast recovery):发送方将慢开始门限ssthresh和拥塞窗口cwnd调整为当前窗口的一半,从而避免执行慢开始算法,降低传输效率。
有时,个别报文段在网络中丢失,但是实际上并未发生网络拥塞,进而错误的执行慢开始算法,从而降低了传输效率。因此,为了避免这种错误判断,进而出现了快重传和开恢复算法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rHHAEqr2-1660988023951)(C:\Users\LLL03\AppData\Roaming\Typora\typora-user-images\image-20220817160325703.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZOjqS2IT-1660988023951)(C:\Users\LLL03\AppData\Roaming\Typora\typora-user-images\image-20220817160705931.png)]
在TCP双方建立逻辑连接关系时,拥塞窗口cwnd的初始值为设置为1,即传输轮次为0时,cwnd的值为1,同时,需要设置一个慢开始门限的初始值,假设为16;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XkR8B4Tt-1660988023952)(C:\Users\LLL03\AppData\Roaming\Typora\typora-user-images\image-20220817160922690.png)]
我们在上⽹的时候,通常使⽤的⽅式是域名,⽽不是 IP 地址,因为域名⽅便⼈类记忆。
那么实现这⼀技术的就是 DNS 域名解析,DNS 可以将域名⽹址⾃动转换为具体的 IP 地址。
DNS 中的域名都是⽤句点来分隔的,⽐如 www.server.com ,这⾥的句点代表了不同层次之间的界限。
在域名中,越靠右的位置表示其层级越⾼。
根域是在最顶层,它的下⼀层就是 com 顶级域,再下⾯是 server.com。
所以域名的层级关系类似⼀个树状结构:
根 DNS 服务器
顶级域 DNS 服务器(com)
权威 DNS 服务器(server.com)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ynuzRsWb-1660988023952)(C:\Users\LLL03\AppData\Roaming\Typora\typora-user-images\image-20220818093830863.png)]
浏览器⾸先看⼀下⾃⼰的缓存⾥有没有,如果没有就向操作系统的缓存要,还没有就检查本机域名解析⽂件
hosts ,如果还是没有,就会 DNS 服务器进⾏查询,查询的过程如下:
客户端⾸先会发出⼀个 DNS 请求,问 www.server.com 的 IP 是啥,并发给本地 DNS 服务器(也就是客户端的 TCP/IP 设置中填写的 DNS 服务器地址)。
本地域名服务器收到客户端的请求后,如果缓存⾥的表格能找到 www.server.com,则它直接返回 IP 址。如果没有,本地 DNS 会去问它的根域名服务器:“⽼⼤, 能告诉我 www.server.com 的 IP 地址吗?” 根域名服务器是最⾼层次的,它不直接⽤于域名解析,但能指明⼀条道路。
根 DNS 收到来⾃本地 DNS 的请求后,发现后置是 .com,说:“www.server.com 这个域名归 .com 区域管理”,我给你 .com 顶级域名服务器地址给你,你去问问它吧。”
本地 DNS 收到顶级域名服务器的地址后,发起请求问“⽼⼆, 你能告诉我 www.server.com 的 IP 地址吗?”
顶级域名服务器说:“我给你负责 www.server.com 区域的权威 DNS 服务器的地址,你去问它应该能问到”。
本地 DNS 于是转向问权威 DNS 服务器:“⽼三,www.server.com对应的IP是啥呀?” server.com 的权威DNS 服务器,它是域名解析结果的原出处。为啥叫权威呢?就是我的域名我做主。
权威 DNS 服务器查询后将对应的 IP 地址 X.X.X.X 告诉本地 DNS。
本地 DNS 再将 IP 地址返回客户端,客户端和⽬标建⽴连接。