计算机启动方式

目前的常用的启动方式有 BIOS & MBR, UEFI & GPT. 其中 MBRGPT 是硬盘分区的两种格式.通常是 BIOS 配合 MBR,UEFI 配合 GPT.

BIOS 和 MBR 的启动方式

在早期的 IBM PC 兼容机中,BIOS 配合 MBR 磁盘分区进行启动.

BIOS 和 MBR 是什么?

BIOS 是 Basic Input/Output System 的缩写,是一种固件程序,一般不能修改. MBR 是 Master Boot Record 的缩写, 这是磁盘上一个特定扇区的名称,通常是HDD(0,0,1),即第一个磁头,第一个磁道,第一个扇区,也使用 MBR 来表示这种磁盘分区方式,以及使用MBR来代称第一个扇区(一个扇区512个字节)的前446个字节,这446个字节是引导操作系统的代码.

MBR 扇区的结构如下:

  • 0x000-0x1B7 : 共 440 个字节, 引导程序
  • 0x1B8-0x1BB : 共 4 个字节, 记录选用磁盘标志
  • 0x1BC-0x1BD : 一般为空值, 0x0000
  • 0x1BE-0x1FD : 记录标准 MBR 格式的分区表规划
  • 0x1FE-0x1FF : 0x55AA MBR 有效标志位

BIOS 配合 MBR 格式的硬盘启动

  • CPU 在上电复位时会将 程序计数器 PC 指针 指向一个地址,这个地址被称为 reset vector, 类似中断向量,这个向量是CPU复位指向的位置.这个时候,主板上的芯片组(Chip set)内或者集成在CPU内部的内存控制器还未初始化,无法访问内存,因此这个地址可以指向 BIOS. BIOS 内的存储程序的存储器被内存控制器映射到内存的地址空间内.CPU根据这个地址执行初始化程序(POST 程序).
  • 初始化内存之后,执行后续的启动程序.
  • 硬件级启动程序完成后,扫描每个磁盘的 MBR 扇区,给予用户选择.
  • 当选择好启动的硬盘后,CPU 仅仅执行 MBR扇区内的启动程序.
  • 通常启动代码只是启动操作系统的一部分,还需要硬盘内其他位置的程序来启动.因为MBR扇区内的空间太少了,难以容纳现代操作系统的启动程序.

UEFI 配合 GPT

什么是 UEFI ?

UEFI (Unified Extensible Firmware Interface), 是一种协议,一种标准,用来沟通操作系统与硬件.

UEFI 目前由 UEFI 论坛管理,许多知名 IT 企业都在这个论坛中.

为什么要引入 UEFI?

由于传统的 BIOS 仅仅是机械地加载硬盘内的 MBR 扇区内的程序,没有从固件级别定义启动的标准,如何启动只取决于 MBR 扇区内几百个字节的程序,这对于现代操作系统来说非常难顶.

UEFI 的引入从固件级别丰富了启动的过程,能够更理想地定义如何启动.

UEFI 与 GPT 的关系

GPT (GUID Partition Table) ,全局唯一标识分区表. 这也是一种硬盘的结构布局. GPT 是 UEFI 标准的一部分.

GPT 的优点

  • 支持空间更大: MBR 单块盘最大 2.2TB , GPT 可以支持 9.4ZB 或者 8ZB.
  • 配合 UEFI 使用更方便.

UEFI 的结构

相较于 BIOS 只读取 MBR, UEFI 可以从硬盘的任意位置执行启动操作系统的代码,也能明确知晓每块硬盘的分区信息。而不像 BIOS 机械地执行代码。

支持 UEFI 的固件必须能够执行特定形式的 EFI 启动二进制文件.

UEFI 从硬盘的任意一个 EFI 分区中读取 EFI 启动二进制文件.这个启动二进制文件可以按照 UEFI 的标准自由编写.

UEFI 在固件内保存有启动管理器, 用户可以通过执行固件内的修改程序,或者在更高层次(操作系统层)修改启动管理器的配置.这样就可以动态地配置启动选择项,每个启动选择项对应执行的 EFI 启动配置文件.

推荐使用 diskgenius 来管理 UEFI 启动项

UEFI 的启动方式

与 BIOS 方式类似,最开始都是加载固件内的程序,扫描所有的PCI-e硬件,装载硬件。

UEFI 根据固件内部的 UEFI 启动管理器的配置,给予用户选择权,用户选择启动项之后,CPU 执行对应 EFI 启动分区内的 EFI 启动二进制文件。

UEFI 的兼容启动项

UEFI 具有兼容启动项。

兼容 BIOS 和 MBR 启动项

UEFI 兼容原有的 BIOS和 MBR 的 启动方式。可以让 UEFI 按照原有 BIOS 的方式读取 MBR 内的程序,然后启动。

Fall Back UEFI 原生启动项

这种方式没有明确地指明启动的 EFI 分区 和启动的 EFI 标准项,而是给予了一个路径,能够类似于正则表达式的模糊匹配,通常适用于一些可插拔的设备或者网络设备的启动。

相当于插头与插座的关系,只是给出了一种模糊的匹配方式,第一个满足需求的目标将会被启动。

UEFI 完全原生启动项

按照 UEFI 标准来的启动项,按照特定 EFI 分区的特定 EFI 二进制可执行启动文件来启动。

Linux 与 Windows 多系统的启动配置问题

一般的2015年之后发布的笔记本或者台式机,预装 Windows 系统的都是采用 UEFI 进行启动。

Windows 系统一般需要三个分区:

  • EFI 分区 (存储 EFI 启动配置文件)
  • MSR 分区 (Microsoft Reserve,微软保留分区,用来保存分区情况,实际上没啥大用处,微软除了用它来保存分区,别的功能都没有)
  • 系统分区

安装 UEFI 双系统

Linux 使用 grub 启动器来启动,grub 启动器是一种 EFI 二进制启动文件。

但很奇怪的是,如果当前 PC 内已经有硬盘具有 EFI 分区,那么在安装 Ubuntu 时,即使在手动分区,选择了 EFI 启动分区,并且把启动文件放在 EFI 分区中时,安装程序也会在已经存在的 EFI 分区(比如 Windows 10 的 EFI 分区中)加载 grub 启动器,并在 UEFI 固件的启动管理器中添加这一项。并不会按照用户的设置去执行。

一般情况下双系统的启动配置

可以通过 grub 启动器启动Ubuntu,同时该启动器具有启动 Windows Boot Manager的能力,进而启动Windows。

也可以通过在开机第一步的 UEFI 启动项选择。

对于已经安装了 UEFI 启动的 Windows 系统的 PC 安装双系统

如上文所述,已经安装了 Windows 的系统中已经有 EFI 分区,Ubuntu 系统安装时,会将 EFI 启动二进制文件安装在 EFI 分区中。

通常这个分区的结构如下:

EFI--Boot--bootx64.efi
     Microsoft -- Boot -- bootmgfw.efi
     ubuntu -- shimx64.efi
            -- grubx64.efi

其中 EFI/Boot/bootx64.efi 是 UEFI 采用Fall Back UEFI 原生启动项方式启动方式下时,按照匹配方式默认匹配的情况使用启动启动文件。

EFI/Microsoft/Boot/bootmgfw.efi 是 Windows 的 EFI 启动的二进制文件。

EFI/ubuntu/shimx64.efi 是针对开启了 安全启动(secure Boot)的情况下使用的 EFI 启动文件。而 EFI/ubuntu/grubx64.efi 则是未开启 安全启动 功能时使用的启动文件。

经过验证(参考:https://www.cnblogs.com/liuzhenbo/p/10807576.html),无论是 Windows 或者 Linux 的安装程序都会修改 EFI/boot/bootx64.efi 这个默认启动 EFI 二进制文件为自己的启动文件。

比如安装了 ubuntu 后,在安装了 EFI/ubuntu/shimx64.efi 后,也会把 EFI/Boot/bootx64.efi 修改为引导自己系统的 shimx64.efi,文件名依然是bootx64.efi 。同理,在重装 Windows 以后,也会把 EFI/Boot/shimx64.efi 修改为 EFI/Microsoft/Boot/bootmgfw.efi,文件名依然是 bootx64.efi

安全启动 (Secure Boot)

安全启动是 UEFI 的标准,即拥有 UEFI 签名的启动文件才能被执行。也就是防止 UEFI 启动文件病毒修改掉,引导到其他地方,保证了启动的安全。

grub 启动器不具有安全签名,不能够被认可。而 Ubuntu 的开发公司 Canonical 取得了安全启动的授权,因此使用 shimx64.efi 可以通过 UEFI 安全启动。

参考的博客:https://blog.woodelf.org/2014/05/28/uefi-boot-how-it-works

asyncio

就是异步 IO,就是协程。

同步IO 和 异步IO

举例子:

import requests as rt
def func():
    resp = rt.get(url = "https://www.baidu.com/")
    print(resp.text)

从 "www.baidu.com" get 内容的时候就是阻塞,因为CPU需要等待网络IO吞吐。

在这样一个函数里面,等待网络IO的时间要远远大于CPU执行实际操作的时间。

函数执行的情况大概如下图:

1# func -*********
2# func           -**********
3# func                      -**********
4# func                                 -*********
5# func                                           -*********

- 为 CPU 执行非等待IO的操作 * 为 CPU 执行等待IO的操作

可以看到,很多的时间浪费在等IO的执行顺序上。

IO 操作

CPU 执行 IO 操作的时候,通常是使用“CPU中断”来执行,也就是CPU通知外部IO设备,当获取到数据的时候,外部IO给CPU一个中断。中断消耗时间比较长。

反正 CPU 等待 IO 的时候也是干等着,不如把时间用在其他操作上。

异步IO

异步IO 就是发出IO请求之后,CPU切换执行代码,执行其他CPU密集的任务,当IO完成的时候,外部中断会通知操作系统,操作系统对IO进行处理,通知进程执行IO回调函数。

函数的执行情况往往如下图:

1# func _<等待OS完成IO操作>^^^
2# func  _<等待OS完成IO操作>*^^^
3# func   _<等待OS完成IO操作>***^^^
4# func    _<等待OS完成IO操作>*****^^^
5# func     _<等待OS完成IO操作>*******^^^

_ 是函数CPU执行时间,<等待IO>*是等待回调函数等待,^^^ 是回调函数的时间。

异步IO常见的实现方式

使用操作系统 API 和事件循环来实现

简单描述:

  1. 当程序函数遇到阻塞的IO的时候,使用操作系统API,例如epoll ,将完成阻塞IO的回调函数引导原函数阻塞IO的地方。
  2. 把所有要执行涉及异步IO的函数都装入列表中,依次执行,由于遇到阻塞IO之后,程序不会阻塞,反而快速执行。因此多个涉及异步IO的函数很快跑完。
  3. 所有的函数执行完毕后,这个时候所有IO还没完成。
  4. 进入循环,遍历所有的函数的操作系统API回调函数的列表,只要有一个完成的,就执行回调函数,返回继续执行。
  5. 循环直到所有函数执行完毕。

Python 基于生成器的异步IO实现

根据之前描述的流程,异步IO的关键在于保存某一个协程的上下文信息(比如变量的值,内存中的位置),而 Python 的生成器恰到好处的具备了异步IO的关键需求。

def func():
    a = 10
    b = yield IO_func()
    a += 1
    print(a)

加入了 yield 关键字,func()就变成了生成器。

执行下面的代码:

func_exe = func()
next(func_exe)
for i in range(10):
      next(func_exe)

输出:

11
12
13
14
15
16
17
18
19
20

每次执行都能保留住上下文(有点像面向对象编程里面的某个实例,具备变量的保存功能)

Python asyncio 库

协程 coroutine

协程是一个函数,用async 关键字声明,定义如下:

async def main():
    print("Hello!")
    await asyncio.sleep(1)
    print("Hello!")

协程里面使用await 关键字来表明阻塞的位置。

# 运行协程不能直接调用
>>> main()
<coroutine object main at 0x1234567>

运行协程 coroutine

运行协程有几种方式:

asyncio.run()

用来直接运行最高等级的协程入口,即使是协程里面定义协程:

async def low_level(delay,what):
    await asyncio.sleep(delay)
    print(what)
async def high_level():
    print("start",time.time())
    await low_level(1,"hello")
    await low_level(2,"world")
    print("End",time.time())
asyncio.run(high_level()) # high_level() 返回一个协程对象 run 函数运行这个协程对象

这样运行函数是顺序运行函数,阻塞的正常阻塞。

输出如下:

start 10:00:00
Hello
world
End 10:00:03

可以观察到,正常运行了3秒钟

asyncio.create_task()

用来并发运行作为 asyncio 任务的多个协程。

async def main():
      task1 = asyncio.create_task(low_level(1,"Hello!"))
    task2 = asyncio.create_task(low_level(2,"World!"))
    
    print("start",time.time())
    
    await task1
    await task2
    
    print("End",time.time())
    
asyncio.run(main())

这样就可以并发地处理 await 里面的任务。

输出如下:

start 10:00:00
Hello
world
End 10:00:02

时间由3秒变为2秒,证明并发运行了 task1 和 task2。

可等待对象

await关键字后面接的都是可等待对象

可等待对象有三种:协程,任务和 Future

asyncio 库整理

基本类型

  • Task 任务
  • Future 协程返回的对象
  • Coroutine 协程

Coroutine 协程

用 async 关键字定义的一个函数,函数内必须包含 await

await 关键字的右面接的是可等待对象。

协程仅仅定义了协程切换时候的关系,不具备配合事件循环进行管理的方法。

Future

future 对象可以理解为一个”占位变量“,提前将要进行异步操作的协程或者任务的返回值表示出来。

future = await coroutine
# 当程序运行到这一行的时候,遇到 await,此时程序切换到别的协程去处理。
# 当 coroutine 完成,结果返回,触发回调,返回这行代码的时候,future 才会接受到变量。

Future 是一个类,类内实现了对正在或者将要在事件循环中运行的协程的管理方法。

比如取消,添加回调函数,保存运行着的协程的状态。

相对较为底层。

直接执行 loop = asyncio.get_event_loop() 而获得的事件循环loop 的操作,往往是配合 future 来用。

Task

Task 类继承于Future 类,是对协程配合事件循环调度的更高层级的抽象,同时具备 future 的基本特性。

为协程添加调度的一些功能,比如取消协程的运行,查看协程是否运行完毕。

对 await 关键字与事件循环的理解

await asyncio.sleep(2)

Coroutine 协程要是能运行起来,必定有一个事件循环 loop,二者密不可分。

事件循环 loop 中注册了各种 Task。

当事件循环正在执行某个Task(即把 coroutine 协程包装起来调度)时,如果遇到 await,将会挂起当前正在执行的 Task,遍历事件循环中未完成的 Task去执行。当 await 关键字右面的 Task 执行完毕,调用 Task 的回调函数时,这个 Task 在事件循环中标记为回调完成,事件循环遍历到这个 Task,发现已经回调完成,将会返回这个位置,继续执行当前 Task,直到遇到下一个 await 则重复上述过程,如果执行到 Task 代码结束(函数定义的结尾),这个 Task 就执行完了,标记 Task 为 finished。

因此想用协程并发的时候,使用 loop.run_until_completeasyncio.run 的时候,必须要有一个 async 函数去作为入口,如下所示:

async def main():
    await asyncio.sleep(1)

loop = asyncio.get_event_loop()
loop.run_until_complete()

事件循环遇到 await 就挂起当前正在执行的协程,切换到 await 的协程,分析事件循环的切换的流程如下:

loop 空循环
main() 协程被加入循环中 
执行 main()协程  
遇到 await 关键字 
挂起 main()协程
进入 asyncio.sleep(1) 协程 ( asyncio.sleep 的本质是调用操作系统函数,在 1 秒后调用回调函数)
协程内注册好定时回调函数之后,立马切换协程到 main()
main() 没有别的代码了 main协程从事件循环中退出
等待 sleep 函数的回调 ....
回调成功 sleep 从事件循环中退出
loop 循环为空循环
执行结束

协程实现并发的本质在于:在执行协程A的时候,遇到await的时候(一般是需要阻塞的等待IO时)跳过,切换到其他不需要等待IO的非阻塞代码,当A的等待IO完成时,CPU可能在执行其他协程的代码。当切换到协程A的时候,返回await的程序位置,使用IO获取的信息,继续执行,直到遇到下一个 await,重复以上过程。当事件循环内没有可以切换的程序的时候,也就是没有其他 await 的时候,事件循环空了,执行就结束了

API

asyncio 分为高层级API低层级API

  • 高层级API 提供了更加抽象的 API ,避免了用户对直接事件循环与任务调度,提高了应用层级。
  • 低层级API 具备更加完善具体的API,能够精准控制,可以用来开发各种底层应用。

高层级 API

async def go(sec):
    await asyncio.sleep(sec)

async def main():
    coro_list = list()
    for i in range(1,5):
        coro_list.append(go(i))
    result = await asyncio.gather(*coro_list)
    # or asyncio.wait()

asyncio.run(main())
  • asyncio.sleep(sec)

    异步暂停

  • Asyncio.gather(*tasks)

    传入可变参数,参数为 task 或者 coroutine 或者 future,用于多个协程并发

    传入 coroutine 之后,coroutine 对象会被包装为 task

    gather 本身是一个普通函数,但调用这个函数的返回值是一个 tasks._GatheringFuturte 对象

    这个对象是个 awaitable 对象,是可以等待的对象。

    同时,gather 可以嵌套调用:

    async def main():
        future1 = asyncio.gather(asyncio.sleep(1),asyncio.sleep(2))
        future2 = asyncio.gather(asyncio.sleep(3),asyncio.sleep(4))
        await asyncio.gather(future1,future2) # 将两个 future 同时加入事件循环并等待
    
    start = time.time()
    asyncio.run(main())
    end = time.time()
    print(end-start)

    输出:

    4.0001127398417

    观察比较上面代码和下面代码:

    async def main1():
        future1 = asyncio.gather(asyncio.sleep(1),asyncio.sleep(2))
        future2 = asyncio.gather(asyncio.sleep(3),asyncio.sleep(4))
        await future1  # 等待 future1
        await future2  # 等待 future2
        
    start = time.time()
    asyncio.run(main1())
    end = time.time()
    print(end-start)

    输出:

    4.00048971298135

    可以发现执行效果相同,可以认为:

    asyncio.gather 就是把 future 加入事件循环中。

    await 所有的 future 就是执行事件循环中的事件

    再注意下面这种写法

    async def main1():
        for i in [1,3]:
            future = asyncio.gather(asyncio.sleep(i),asyncio.sleep(i+1))
            await future
       
    start = time.time()
    asyncio.run(main1())
    end = time.time()
    print(end-start)

    输出:

    6.000001315871293

    执行时间由原来的4秒变为了6秒,这也说明了没有实现异步,程序是串行执行的。

    观察代码,这个 for 循环执行了两次,当第一次 for 循环的时候,虽然 await 了 future ,但此时事件循环中只有这一个 future,只能等待执行完,事件循环切换到异步的 main 函数了,再把 asyncio.gather(asyncio.sleep(i),asyncio.sleep(i+1)) 这个 future 加入事件循环并切换到这个协程里面,重复之前的步骤执行。

    gather 函数的返回值就是按照参数顺序的 coroutine 或者 task 的返回值**

    如果 gather 嵌套,返回值顺序是嵌套树的先序遍历。

    async def sleep(sec):
        await asyncio.sleep(sec)
        return sec
    
    async def main1():
        future1 = asyncio.gather(sleep(1),sleep(2))
        future2 = asyncio.gather(sleep(3),sleep(4))
        result1 = await future1 
        result2 = await future2 
        return result1 + result2
        
    start = time.time()
    result = asyncio.run(main1())
    end = time.time()
    print(end-start)
    print(result)

    输出:

    4.002225399017334
    [1, 2, 3, 4]
  • asyncio.create_task & asyncio.ensure_future

    二者返回值都是一样的,都是包装 coroutine 或者 future,返回一个 pending 状态的task。

    区别是:

    asyncio.create_task 是获得正在运行中的事件循环 loop ( asyncio.get_running_loop ) ,再调用 loop.create_task 将可等待对象加入 loop 中。

    往往定义在 协程 的内部,再通过 asyncio.run 这个高级 API 进行调度。

    不推荐混用高等级 API 和 低等级 API。

    在异步函数(协程)外面使用 create_task 往往不行,因为此时没创建事件循环,或者说事件循环没有控制协程的执行。

    asyncio.ensure_future 则是 asyncio.get_event_loop ,在异步函数(协程)外部定义是可以的。

    处理机制类似,最后也是调用 loop.create_task ,将可等待对象包装为 task 加入 loop 中。

    推荐使用 asyncio.ensure_future ,对错误包容度更强。

低级 API

低级 API 中,事件循环是整个操作的主体对象,通过 asyncio 获得事件循环。

  • asyncio.get_event_loop( )

    通常使用这个函数,get_event_loop 中封装了 get_running_loop , new_event_loop , set_event_loop 等函数。
  • loop.create_task vs loop.create_future

    loop.create_task 为事件循环添加 task ,是 asyncio.create_task 的封装的函数。

    输入参数就是可等待对象,包括协程或者future

    loop.create_future ,没有输出参数,就是实例化一个 future ,绑定在当前 loop 上。

    一般用于最底层的配置,比如给 future 绑定回调函数,给 future 添加 call_soon 等等功能。

  • loop.run_until_complete

    运行一个协程,Task 这类的可等待对象直到完成。

    这个是 asyncio.run 内部封装的函数,可以作为启动协程的入口函数。

并发的几种常见启动方式

1 标准并发的启动方式

import asyncio

async def main():
    task_list = list() 
    for i in range(1,5): 
        task_list.append(asyncio.ensure_future(asyncio.sleep(i)) # 添加 task 进入
    await asyncio.gather(*task_list) # 使用 gather
                         
# 方式1:
asyncio.run(main())
# 方式2:
loop = asyncio.get_event_loop()
loop.run_until_complete(main())                         

2 自主启动方式

import asyncio

async def main():
    task_list = list()
    for i in range(1,5):
        task_list.append(asyncio.ensure_future(asyncio.sleep(i)))
    for each in task_list :
        await each
# 方式1:
asyncio.run(main())
# 方式2:
loop = asyncio.get_event_loop()
loop.run_until_complete(main()) 

一点小思考

即将要写的:

asyncio.create_task & asyncio.ensure_future & loop.create_task

如果在一个循环上面注册了多个 task ,只要注册上了,当 await 其中的一个被挂起,没有接在 await 关键字后面的 task 就不会被切换进入,不 await 就不会收到结果。

即将要写的 同步改异步

实际上是多线程,真搞协程要从底层搞起。

真正的协程写起来,需要调用操作系统函数,从底层去适配阻塞的问题。

如果同步的函数中的阻塞代码需要以协程的方式并发使用,往往需要多线程来实现。

应用asynio.get_event_loop.run_in_executor来将同步函数改为”异步“:

import asyncio
import concurrent.futures.ThreadPoolExecutor # 线程池
import time
from functools import partial
threadpool = ThreadPoolExecutor(max_workers=100)
async def sleep(sec):
    await asyncio.get_event_loop().run_in_executor(partial(time.sleep,sec))
    # 当执行到 await 的时候,把这个函数塞给线程池,让线程池去调度

async def main():
    task_list = []
    for i in range(1,5):
        task_list.append(sleep(i))
        
asyncio.run(main())

这样就能以多线程的本质使用协程的方式去编写代码。

有关 pip 是什么的问题

我之前一直认为 pip 是一个独立的二进制文件,但实际上 pip 也是 python 的一个 module ,一个模块,一个库。

使用 pip 通常有两种方式,一种是:

# 方式1
pip3 install requests

# 方式2
python3 -m pip install

在类 Unix 系统下

查了查 pip ,实际上 pip 是一个脚本,截取一段放一下:

对于 Python3.8 :

#!/usr/local/python38/bin/python3.8
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
    sys.exit(main())

实际上就是一段调用 pip 的脚本。

在 Windows 系统下

Windows 下,pip 和 python 一样是一个可执行文件。

一般位于 C:/Program Files/Python/Scirpts/pip.exe 的位置。

如果删除了 Python 安装目录下面的 python.exe ,执行pip.exe ,则会报错,不能启动相关进程。

个人分析,应该是 Windows 在安装 Python 的时候,根据 Python 安装的目录,编译出来的 pip.exe 但实际上还是调用了 python.exe

(但另外一种想法是根据 环境变量来操作)

有关 pip 在默认在哪里安装模块的问题

之所以会讨论 pip 默认安装目录,是因为装了两个版本的 Python:

安装情况如下:

  • Python2.7 : Debian 系统自带
  • Python3.7 : Debian 系统自带
  • Python3.8 : 手动安装

python2 在 Debian 系统中是很多工具的依赖,一般就当个依赖来看了,也不怎么用。

系统自带的 Python3.7

相关的库安装的目录十分分散,符合一个在 /usr/bin 目录下系统级应用程序的安装标准。

简单的列一下相关目录:

  • /usr/lib/python37.zip : 用途未知,也不存在这个文件/目录
  • /usr/lib/python3.7 : Python 标准库
  • /usr/lib.python3.7/lib-dynload : 用别的语言写的动态加载库
  • /usr/local/lib/python3.7/dist-packages : 使用对应版本 pip 或用 apt 安装的库
  • /usr/lib/python3/dist-packages : 通用的库

自己安装的 Python3.8

在编译并安装 Python 的时候,使用 ./configure 文件进行相关的配置,可以配置可执行文件(通常是/bin目录),其他文件(除了可执行文件之外的),库文件 (/lib)的位置,可以像系统自带的 Python 那样把各个部分分开安装,也可以放在一个目录里(通常放在一个目录里,方便快速删除)。

假设配置的时候设置的前缀: ./configure --prefix=/usr/local/python38 ,目录下面:

  • /usr/local/python38/bin : 存放二进制文件的,如果通过 pip 安装的库或者模块带有可执行文件,一般都放在这里。
  • /usr/local/python38/share : 存放着 python 的 manual ,通过 man 指令调用。
  • /usr/local/python38/lib : Python 的库目录,标准库在这里
  • /usr/local/python38/lib/site-packages : 通过对应 pip 安装的目录

    影响 pip 安装目录的因素

因为 pip 实际上是通过 python 调用的脚本,因此 pip 的配置与 python 的一些配置高度相关。

sys.path

调用 sys 模块的 path 方法,可以 python 寻找一个模块(包)的路径。

>>> import sys
>>> sys.path
['', '/usr/local/python38/lib/python38.zip', '/usr/local/python38/lib/python3.8', '/usr/local/python38/lib/python3.8/lib-dynload', '/usr/local/python38/lib/python3.8/site-packages']

'' 这个空字符串代表着脚本所在的目录,剩下的就是寻找包的路径,sys.path 这个数组的头到尾,优先级依次降低。

也就是如果在更高优先级的目录找到了一个模块,那么在低优先级的目录就不会去寻找,尽管低优先级目录也有。

site.py

详细配置参考 : https://docs.python.org/3/library/site.html

也是标准库,每次 python 脚本开始运行的时候,如果不加 -s 去调用,都会执行 site.py

这个库文件位于标准库的目录内,用来装载 sys 模块下的各个参数。

sys.path 有关的执行就在site.py 里面。

执行site.py 时,文件会从 sys 模块获取两个前缀:sys.prefixsys.exec_prefix

通常在 类 Unix 平台上,二者相等。举例子,如果 Python 安装在 /usr/local/python38 ,则:

>>> sys.prefix
'/usr/local/python38'
>>> sys.exec_prefix
'/usr/local/python38'

根据这两个前缀,site.py 会使用已经设定好的四个后缀去跟两个前缀拼接,并检验拼接出来的路径是否存在:

(X.Y) 为 python 的版本,例如 Pythion3.8

Suffix_1 : /lib/pythonX.Y/

Suffix_2 : /lib/pythonX.Y/lib-dynload

Suffix_3 : /lib/pythonX.Y/site-packages

Suffix_4 : /local/pythonX.Y/

Suffix_5 : /local/pythonX.Y/lib-dynload

Suffix_6 : /local/pythonX.Y/site-packages

如果存在,则把目录加入 sys.path 这个 list 中,如果不存在,则忽略。

建议根据自己的 site.py 文件查看拼接的字符串,不同版本的 python 源码包可能附带的不一样。

同时,site.py 还能从环境变量中读取 $HOME 变量,获得当前用户的家目录。

也会在 sys.path 中加入一个 $HOME/.local/lib/pythonX.Y/site-packages ,一般都加在 sys.path 的最后(前提是 $HOME/.local/lib/pythonX.Y/site-packages 存在)

*.pth文件

这个文件提供了一种客制化 sys.path 的可能性,这个文件放在上文提到的那些后缀的目录下面,site.py 会读取这些文件,文件内存着想要加入sys.path 的目录。

按照字典序执行*.pth ,先出现的路径会先加入 sys.path 中,具备更高的优先级,索引值更小。重复出现同一目录,优先级不受影响,即不会在 sys.path 中删除原有的路径。

usercustomize.py 和 sitecustomize.py 文件

site.py 在执行完上面那些配置后,会尝试 import sitecustomize.py 文件,如果这个文件存在,那么文件会正常 import,如果文件不存在,Python raise 的 ImportError 会被忽略。

site.py会根据site.ENABLE_USER)CUSTOM 标志位的 true or false 来决定是否要在加载完 sys.path 之后再 import usercustomize.py

权限与安装目录问题

如果用户 A 使用 pip 安装模块,但 sys.path 这个 list 中索引较小的目录用户A不具备写入的权限,那么 pip 会寻找索引最小的具有写入权限的目录去写入。

常见的情况:

/usr/lib/python3.8 的权限为 : drwxr--r--,非 root 用户使用 pip 的时候,sys.path 前面几个目录没有权限用不了,这个时候一般会降级到用户家目录: ~/.local/python3.8/lib下面。

所以一般都使用 root 安装包,除非想安装在用户目录下。