YiaYiaO

唐老鸭有一个农场,咿啊咿啊噢


  • 首页

  • 标签

  • 分类

  • 归档

Python学习总结06——multiprocessing

发表于 2019-08-05 | 更新于 2019-08-13 | 分类于 Python , Basic

Python的多进程,官网上multiprocessing的文档,比我在网络上看到的其他一些博客和教程写得都要好,给出链接:点击这里,建议浏览英文文档,或者中英文对照看。本文以下的内容,我基本上按照官方文档的结构进行展开,对其中部分内容给出自己的理解与总结。

概述

multiprocessing is a package that supports spawning processes using an API similar to the threading module. The multiprocessing package offers both local and remote concurrency, effectively side-stepping the Global Interpreter Lock by using subprocesses instead of threads. Due to this, the multiprocessing module allows the programmer to fully leverage multiple processors on a given machine. It runs on both Unix and Windows.

这里我直接将官网原文引用了过来,部分单词加粗强调当然是为了引起重视,multiprocessing 模块是支持创建进程,注意是支持,而不是用于,它包含了创建进程的能力,同时也提供了本地和远端的 python 进程同步的能力。如果说 multiprocessing 就是用来生成多进程的,显然不全面;同时注意一下文档有写它支持本地与远端进程间的同步,并不意味着它可以在远端服务器上创建进程。

进程的启动

在 multiprocessing 中,通过创建一个 Process 对象然后调用它的 start() 方法来生成进程,根据不同的平台, multiprocessing 支持三种启动进程的方法:spawn、fork 和 forkserver。spawn 直接新启动一个 python 解释器进程;fork 和 forkserver 都是调用了 unix 底层的 fork 函数,区别是 fork 会对当前进程的资源进行拷贝,而 forserver 先创建一个所谓 server 进程,再对 server 进程进行 fork,这样forserver 创建的子进程就不包含当前进程资源的拷贝。

尝试执行以下的代码,将第8行 get_context 方法中的参数分别修改成 forkserver 和 spawn,能看到执行结果的差别。还可以进一步尝试将第6行的“if name == ‘main‘:” 去掉,看程序能否正常执行,分别采用 spawn、fork 和 forkserver 启动进程时有什么不同的执行结果。

1
2
3
4
5
6
7
8
9
10
import multiprocessing

def f():
print(q)

if __name__ == '__main__':
q = 1
ctx = multiprocessing.get_context('fork')
p = ctx.Process(target=f)
p.start()

当线程的启动方式为 fork 时,以上的程序能够正常执行并且打印结果为1,原因就是 fork 创建的子进程对父进程进行了拷贝,其中就包含 q,其值为1。

以上的程序通过 get_context() 来获取 context,使用 context 来设置进程的启动方式,这也可以通过 multiprocessing.set_start_method() 来设置,但是 set_start_method() 函数的效果是全局性的,当你的程序需要打包成 package 提供给别人使用时,建议使用 context 。还需要注意文档中使用 context 创建的对象在别的 context 创建的进程中可能不兼容,理解这句话可以参考以下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import multiprocessing

def producer(q):
for i in range(10):
q.put("包子 %s" % i)
print("开始等待顾客买包子...")
q.join()
print("所有的包子被取完了...")

def consumer(n):
while q.qsize() == 0:
pass
while q.qsize() > 0:
print("%s 买到" % n, q.get())
q.task_done()

if __name__ == '__main__':
ctx = multiprocessing.get_context('spawn')
q = ctx.JoinableQueue() # 将这一行修改成 multiprocessing.JoinableQueue()试试
p = ctx.Process(target=producer, args=(q,))
p.start()
c1 = consumer("小王")

队列,管道和共享内存

python进程的队列,是一个被封装的很重的东西,官网上有下面一段note:

Note: When an object is put on a queue, the object is pickled and a background thread later flushes the pickled data to an underlying pipe. This has some consequences which are a little surprising, but should not cause any practical difficulties – if they really bother you then you can instead use a queue created with a manager.

进程 queue 基于底层的 pipe 事项,当数据放入队列和从队列取出时,它经历了一个序列化和反序列化的过程。当一个东西经过了过多的封装,我就比较倾向于认为它不太可靠,或者性能上存在问题,或者功能上尤其是并发上可能存在缺陷。不过在注重功能实现,不需要考虑工程压力的时,进程 queue 还是非常简单方便的,它的用法基本和线程 queue 相同,就不再赘述了,可以参考官网。

进程间通信,基本上适用以下的几个套路:

1.socket: 比较底层,想用肯定能用上,几乎所有的语言都支持,实现比较复杂
2.共享内存: 依赖了语言和框架,比如 python 可以在父子进程设置共享内存
3.pipe: 以来系统,语言和框架

个人总结,仅供参考

接下来着重看一下 pipe 和共享内存:

管道 Pipe

multiprocessing.Pipe([duplex]) 返回一对 Connection 对象 (conn1, conn2) 分别代表管道的两端,如果 duplex 传 True,那么这个管道是双向的,如果 duplex 传 False,那么该管道为单向的,单向的管道 conn1 只能被用于接收信息,conn2 只能被用作发送信息。想要用会 Pipe,需要熟悉 Connection 类,此外正如官网所介绍的,multiprocessing.connection 还有一些更加灵活高级的用法,了解它们可以帮助你应对更加复杂的场景。

1
2
3
4
5
6
from multiprocessing import Pipe
a, b = Pipe()
a.send([1, 'hello', None])
print(b.recv())
b.send_bytes(b'thank you')
print(a.recv_bytes())

通过上面的例子,可以看到 Pipe 的使用并不是和多进程绑定在一起的,上例中使用了 Pipe 但是没有创建多进程,python 其他很多模块设计也体现着这一特点。

共享内存

It is possible to create shared objects using shared memory which can be inherited by child processes.

可以通过使用共享内存创建共享对象,在线程与其创建的子线程之间进行共享。内存共享通过创建 ctypes 对象来达成,在 multiprocessing 模块下,有 Value 和 Array 两个函数返回用于共享内存的 ctypes 对象;在 multiprocessing.sharedctypes 模块下,有 RawArray、RawValue、Array、Value 四个函数返回共享内存的 ctypes 对象。

我没太搞懂 multiprocessing.Value 和 multiprocessing.sharedctypes.Value 之间的区别

具体的实例请参加:官网

Manager

Managers provide a way to create data which can be shared between different processes, including sharing over a network between processes running on different machines. A manager object controls a server process which manages shared objects. Other processes can access the shared objects by using proxies.

谈到 Manager 模块,这可以说是 Python 在进程共享数据上大招,它提供的进程间的数据共享,不仅仅限于 Python 的父子进程间,还包含本机不用的 Python 进程间的数据共享,以及两个不同机器的进程间通过网络的数据共享。概述中提到的 multiprocessing 提供了远程进程间的数据共享能力,指的就是它。

目前我知识将官网上的 Manager 一节粗浅的浏览了一下,深入的学习和总结留待以后用到再说了,因为确实 Manager 相对复杂一些,知识点更加细节。如果仅仅是需要创建子进程,实现数据同步,或者进程间同步,不依赖 Manager 也能实现;如果有场景,需要在多个不相干的 Python 进程间进行通信,可以考虑它。

Programming guidelines

最后建议看一下官网的 Programming guidelines,里面有一些非常好的编程建议以及最佳实践,唯一的缺憾是目前还没有中文翻译。

有时间的话我来翻译一下

Python学习总结05——threading

发表于 2019-08-01 | 更新于 2019-08-04 | 分类于 Python , Basic

了解进程与线程

想要了解线程,就绕不开进程。

什么是进程

An executing instance of a program is called a process.
Each process provides the resources needed to execute a program. A process has a virtual address space, executable code, open handles to system objects, a security context, a unique process identifier, environment variables, a priority class, minimum and maximum working set sizes, and at least one thread of execution. Each process is started with a single thread, often called the primary thread, but can create additional threads from any of its threads.

翻译:一个正在执行的程序的实例被称之为进程。每一个进程都提供了程序允许所需要的资源,一个进程包含该一个虚拟地址空间,可执行的代码,连接系统其他对象的handle,一个安全上下文,一个独立的进程编号,环境变量,优先级(priority class),最大和最小的工作空间,以及至少一个正在执行的线程。每一个进程启动时都伴随了一个线程,通常称之为主线程,进程中的每一个线程都可以再创建其他的线程。

什么是线程

A thread is an execution context, which is all the information a CPU needs to execute a stream of instructions.

翻译:线程是一个执行的上下文,包含了CPU需要执行的一系列指令的所有信息。

个人翻译未必准确,建议参考英文

以上是我能够查到的,对进程和线程的比较官方比较权威比较可信的解释,这句话我琢磨了比较久,用了三个比较,因为计算机到底是一门人造学科,很多概念在大家定义和实践它时,在不同人之间有所差别。以上是一个比较通用,大家比较接收的定义,写Python时用到的进程和线程完全符号以上定义,而对Linux系统来说有人就有了其他的看法,比如“Linux没有线程”,在Windows系统上讨论以上的进程和线程的定义就又不一样了。

从网上看到了一篇讲Linux的进程与线程的通俗易懂的好文,链接请点这里,我认为Linux是有线程的,部分观点认为Linux没有线程,因为创建进程和线程都调用系统底层的clone接口,只是参数不同。但是我看到这一点参数的不同给创建的进程与线程带来了实质的差别——是否与父进程共享空间。另外链接中的博文里面也提到创建线程时并不采用clone系统调用,而是采用线程库函数。常用线程库有Linux-Native线程库和POSIX线程库。其中应用最为广泛的是POSIX线程库。因此读者在多线程程序中看到的是pthread_create而非clone。

我对Linux操作系统实现知之甚少,很多浅见都是网上浏览所得,期待以后能够系统性的学习

脱离开具体的场景来讨论问题的经常是没有意义的,创造一个概念在生搬硬套现实的场景同样容易产生谬误。关于进程和线程,还有一个一句话定义:线程是最小的执行单位,进程是最小的分配资源单位。这句话固然精辟,高度概括,但我认为它还是一个总结,是加深理解的一个维度,而不是不可反驳真理。

Python线程

先划一下重点,Python真的很简单,线程相关的知识点不外乎下面几个,如果都能想起来,下文就不用继续看了。

  1. Python创建线程有两种方式,一种是创建Thread对象,传入要运行的方法;另一种是继承Thread类,重写其中的run方法。
  2. 通过将子线程设置为守护线程,setDeaon(True),可以让主线程结束时子线程也同时结束。
  3. 通过join方法让父线程等待子线程执行结束,注意如果同时创建了多个子线程,这些子线程需要并行的执行,需要在这些子线程的start方法都已经被调用了之后,再调用每个子线程的join函数,否者这些子线程间的执行将变为串行。
  4. 线程锁,Lock和Rlock,Lock为指令锁,不可重入,RLock为可重入锁
  5. 线程间交互,Condition和Event
  6. 队列queue

以下分别讲述给个知识点,以代码为主:

线程创建:

  1. 通过创建Thread对象创建子线程
1
2
3
4
5
6
7
8
9
10
11
12
import threading
import time

def run(n):
print("task", n)
time.sleep(2)

t1 = threading.Thread(target=run, args=('t1',)) # 传参为元组,逗号为必须
t1.start()
t2 = threading.Thread(target=run, args=('t2',))
t2.start()
print(threading.active_count()) # 结果为3,1个主线程,2个子线程
  1. 通过继承Thread类并且重写run方法创建子线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import threading
import time

class MyThread(threading.Thread):
def __init__(self, n, name):
super(MyThread, self).__init__()
self.n = n
self.name = name

def run(self) -> None:
print("running task ", self.n)
time.sleep(2)

print(threading.current_thread()) # 打印当前线程名称, MainThread
t1 = MyThread(1, '线程1')
t2 = MyThread(2, '线程2')
print(threading.active_count()) # 打印结果为1,1个主线程
t1.start()
t2.start()
print(threading.active_count()) # 打印结果为3,1个主线程,2个子线程

setDaemon方法和join方法

以下的一段程序来自网络(链接):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import time
import threading

def run(n):
print('--running--', n)
time.sleep(2) # 尝试修改此处的sleep时长,看程序执行结果有什么影响
print('--done--')

def main():
for i in range(5):
t = threading.Thread(target=run, args=[i, ])
print('starting thread', t.getName())
t.start()
t.join(timeout=3) # 尝试修改此处的join时长,看程序执行结果有什么影响

m = threading.Thread(target=main, args=[])
m.setDaemon(True) # 将此处的setDaemon(True)去掉,程序执行结果有什么影响
m.start()
m.join(timeout=5)
print("---main thread done----")

程序执行结果如下:

可以看到,1.因为第14行join的存在,Thread-2、Thread-3与Thread-4这些子线程之间变成了串行执行;2.因为17行的m.setDaemon(True),当主线程退出后,变量m引用的线程立即退出,线程Thread-4也立即退出。Thread-4的立即退出,说明对于Thread-4而言,Deamon也是True。

修改第6行的sleep时长为3,修改第14行的timeout时长为2,执行程序结果如下:

可以看到,因为join等待的时长小于函数执行的时长,所以没有等Thread-2执行结束,Thread-3就开始执行了。尝试在13行插入t.setDaemon(False),又有不同的执行结果,可以看到当Thread-2、Thread-3与Thread-4这些子线程设置了Daemon为False,它们的父线程m需要等到它们都执行结束了才会退出,结果不在这里贴出来了,请自行尝试。

Lock和Rlock

Python线程锁,网上的示例很多,这里只给出一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import threading

def run():
global num
# lock.acquire() # 注释掉锁之后,最末行print的结果小于100
tmp = num + 1
print(tmp) # 这里需要做些什么,否则很难复现
num = tmp
# lock.release()

num = 0
lock = threading.Lock()

threads = []
for i in range(100):
t1 = threading.Thread(target=run, args=())
t1.start()
threads.append(t1)

for t in threads:
t.join()

print(num)

Condition和Event

线程间同步,对应场景类似两个人同时在干活,B需要在A完成特定的工序之后,才能继续后面的工作,B怎么知道A已经完成呢?也无外乎两种方式,第一种是A做完了通知B,期间B一直在休息区坐着;另一种是B隔一会就过来看一下,这种方式下A已完成工作这个信号需要时确切无疑的,当B过来看的时候,A不能表示我马上就好,或者还有5分钟就好,而应该明确告诉B我已经好了或者我还没好。对应的,Condition的方式类似A结束了通知B,期间B在休息室里;Event的方式就类似B一直在边上看着。

Condition的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import threading
import time

con = threading.Condition()
num = 0

class Producer(threading.Thread): # 生产者
def __init__(self):
threading.Thread.__init__(self)

def run(self):
global num
con.acquire() # 锁定线程
while True:
print("开始添加!!!")
num += 1
print("火锅里面鱼丸个数:%s" % str(num))
time.sleep(1)
if num >= 5:
print("火锅里面里面鱼丸数量已经到达5个,无法添加了!")
con.notify() # 告诉休息区的小伙伴开吃
con.wait() # 自己进去休息区
con.release() # 释放锁

class Consumers(threading.Thread): # 消费者
def __init__(self):
threading.Thread.__init__(self)

def run(self):
con.acquire()
global num
while True:
print("开始吃啦!!!")
num -= 1
print("火锅里面剩余鱼丸数量:%s" %str(num))
time.sleep(2)
if num <= 0:
print("锅底没货了,赶紧加鱼丸吧!")
con.notify() # 告诉休息区的小伙伴开始生产
con.wait() # 自己进入休息区等待
con.release()

p = Producer()
c = Consumers()
p.start()
c.start()

代码示例转载自网上,链接:上海-悠悠。

以上是一个简单的生产者消费者示例,首先我们注意到13行和31行con.acquire(),可以动手试一下把它注释掉会怎么样,con.acquire()和con.release()的组合可以采用with语句进行替换,代码会更加简洁。然后,notify和wait的顺序不能换,不能干完活不通知对方一声自己就进入等待区了,这样会形成阻塞;但是如果将以上代码21行和22行的notify和wait的顺序交换,程序还是能正常执行,这是因为锁的原因,可以自己尝试一下,思考为什么会这样。

然后试想一下我们能否通过线程锁实现该模型,让两个线程竞争一把锁,谁拿到锁谁工作,工作完了释放锁?这样是可行的,但是需要加一些手段,为此我写了下面一段代码。关键就在于代码的第22行和第40行,当前获取了锁的进程工作完毕之后,需要sleep一下再竞争锁,否则很可能自己刚刚释放掉锁,又被自己拿到了,那整个流程就自己一个人在干活了,没别人什么事了。显然每次都sleep一下显得很low,所以设计出condition是有道理的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import threading
import time

lock = threading.Lock()
num = 0

class Producer(threading.Thread): # 生产者
def __init__(self):
threading.Thread.__init__(self)

def run(self):
global num
lock.acquire() # 锁定线程
while True:
print("开始添加!!!")
num += 1
print("火锅里面鱼丸个数:%s" % str(num))
time.sleep(0.5)
if num >= 5:
print("火锅里面里面鱼丸数量已经到达5个,无法添加了!")
lock.release() # 释放锁
time.sleep(2)
lock.acquire() # 锁定线程

class Consumers(threading.Thread): # 消费者
def __init__(self):
threading.Thread.__init__(self)

def run(self):
global num
lock.acquire() # 锁定线程
while True:
print("开始吃啦!!!")
num -= 1
print("火锅里面剩余鱼丸数量:%s" %str(num))
time.sleep(0.5)
if num <= 0:
print("锅底没货了,赶紧加鱼丸吧!")
lock.release()
time.sleep(2)
lock.acquire() # 锁定线程

p = Producer()
c = Consumers()
p.start()
c.start()

至于Event,不得不再提一下python很简单,还是直接看示例吧,来自网络:金角大王Alex-Python之路,Day9, 进程、线程、协程篇。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import threading,time
import random
def light():
if not event.isSet():
event.set() #wait就不阻塞 #绿灯状态
count = 0
while True:
if count < 10:
print('\033[42;1m--green light on---\033[0m')
elif count <13:
print('\033[43;1m--yellow light on---\033[0m')
elif count <20:
if event.isSet():
event.clear()
print('\033[41;1m--red light on---\033[0m')
else:
count = 0
event.set() #打开绿灯
time.sleep(1)
count +=1
def car(n):
while 1:
time.sleep(random.randrange(10))
if event.isSet(): #绿灯
print("car [%s] is running.." % n)
else:
print("car [%s] is waiting for the red light.." %n)
if __name__ == '__main__':
event = threading.Event()
Light = threading.Thread(target=light)
Light.start()
for i in range(3):
t = threading.Thread(target=car,args=(i,))
t.start()

队列queue

python的queue,与c++标准库中的queue类似,但是它的get和put两个函数,都有一个block的形参,而且默认为True,这使得它非常适用与线程间的交互,比如只需要简单改写一下上面的例子,它就可以代替event。网上相关的资料很多,不在这里赘述了。

python-learning-03

发表于 2019-08-01

python-learning-02

发表于 2019-08-01

Python 模块 subprocess

发表于 2019-07-25 | 更新于 2019-07-27 | 分类于 Python , module

谈一下自己学习subprocess模块和自己实践的过程中,遇到的一些问题与思考体会。先给出:官网链接

首先从名字说起,模块名称中包含process,就说明了其与线程的联系,官网中对于这个模型的介绍可谓相当的精辟,建议在学习这个模块的用法前多阅读几遍加深理解:

The subprocess module allows you to spawn new processes, connect to their input/output/error pipes, and obtain their return codes. This module intends to replace several older modules and functions.

然后,关注到这个模块最主要的两个函数是 subprocess.run() 和 subprocess.Popen(),它们有什么差别?我从官网粗略看了一下,没太理解为什么要同时设计 run 和 Popen;但是从官网一些实现的描述,我注意到 run 内部封装了一个 Popen。其实再仔细看一下,就可以发现最直接的一点差别,run 是一个函数,Popen 是一个类。再进一步,run 函数返回的对象类型是 subprocess.CompletedProcess,直白一点,run 函数直接获取到了子进程执行的结果;相比之下,Popen 类允许通过 stdin, 与子进程进行交互。

Part 1.

关于 subprocess.run() 函数,我想用一个简单示例结束战斗,python 用起来本来就很简单,更为丰富的使用方法可以参考官网。

1
2
3
4
5
import subprocess
ret = subprocess.run(["ls", "-lrt"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(ret.returncode)
print(ret.stdout.decode())
print(ret.stderr.decode())

Part 2.

对于 subprocess.Popen 类,它面向的整个与子进程的交互过程,问题来了,交互过程何时开始,何时结束?测试如下的代码的执行:

1
2
3
4
import subprocess
import time
proc = subprocess.Popen(["pwd"], stdout=subprocess.PIPE)
time.sleep(10)

代码第4行 time.sleep(10) 让程序在 Popen 执行之后 sleep 10秒,在程序启动的10秒内和程序执行结束后,两次在Ubuntu终端下执行 ”ps -ef | grep pwd” 命令,可以看到如下执行结果:

从上文的执行结果分析,程序第3行 subprocess.Popen 启动了pwd子进程,pwd子进程很快执行结束,而因为主进程没有执行 wait 调用,子进程变成了 defunct 僵尸进程,直到整个测试程序执行结束。为了避免子进程变成 defunct 僵尸进程,在 Popen 执行之后,需要调用 proc.wait(或者proc.communicate)。

我认为 proc.communicate 比 proc.wait 更值得推荐,原因我关注到了这么两点:首先在子进程输出到PIPE,并且输出太多超过了系统 buffer 空间大小时,wait 函数将会阻塞,而 communicate 函数没有类似的问题(具体的原因可能需要借助源码的学习了);其次 proc.communicate 直接返回子进程的 stdout 和 stderror,在这个过程中 proc.communicate 主动对 stdout 和 stderror 的 PIPE 进行了 close,而 proc.wait 函数需要你主动进行 close 。

Part 3.

上面一节我们还是没有提到与子进程进行交互,在使用 Popen 时,我们可以通过子进程的标准输入输出,与子进程进行交互,示例如下:

test.py
1
2
3
while True:
data = input().upper()
print(data)
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import subprocess
proc = subprocess.Popen(["python3", "test.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
while True:
data = input('please input >')
proc.stdin.write(data.encode()) # 写入字节
proc.stdin.write(b'\n') # 写入换行符,子进程每次读取一行
proc.stdin.flush() # 需要flush,子进程才能立即接收
line = proc.stdout.readline()
print(line)
except KeyboardInterrupt:
print("user closed...")
finally:
proc.kill()
proc.wait()
proc.stdin.close()
proc.stdout.close()
proc.stderr.close()

以上的 main.py,创建子进程调用 test.py,实现了将输入的字符转换为大写之后再输出的功能,留意代码中的注释部分,与子进程进行交互时,需要写入换行符,并且刷新PIPE。

以上的进程间交互有一个问题,如果子进程的输出不止一行怎么办,注意千万不要将 proc.stdout.readline() 放到一个循环里面,当子进程已经完成本轮输出之后,主进程还在读,便会形成阻塞。这个问题怎么解决呢?老实说我也不知道,查找了一些材料也没有找到破解的方法,或许主进程知道子进程每次交互的输出大小,或者子进程能正常退出是唯一的解决方法。因为这个问题不知道怎么解,所以应用的场景也就受到了限制,应用在子进程的执行结果较为确定的场景应该还是不错的,比如对子进程进行工具测试。

Python学习总结04——socket

发表于 2019-07-14 | 更新于 2019-08-01 | 分类于 Python , Basic

写在前面的

忘记了是在大学的Java课上,还是在计算机网路课上,第一次接触Socket,当时似懂非懂,有一种不明觉厉的感觉,后来毕业找工作笔试的时候还想拿出来秀一下,Copy网上的实现写了一个最为简单的Socket,然而并没有什么卵用。我们学习的时候,还是要务实一些,尽量搞清楚一个东西的原理以及应用场景,不然头脑里只是一些范范的知识,只会瞎扯和空谈,是没有任何用处的。

那么Socket到底是什么呢,它是编程接口,是对TCP/IP的封装,Python官网Socket一节有一个副标题“Low-level networking interface”,直译过来就是:底层的网络接口。Unix、Windows和MacOS系统均提供了基于BSD规范的Socket接口,Python的Socket类其实是对系统Socket接口的进一步封装。通过Socket,我们可以实现应用层的一些协议,例如ftp和http,或者定义一套自己通信标准,从而实现本机进程间乃至不同主机间的通信。

不得不说,Python将Socket封装的很好,以下是一个简单的Socket实现。

从实例开始

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import socket

server = socket.socket()
server.bind(('0.0.0.0', 6971))
server.listen(5)

while True:
conn, addr = server.accept()
print(conn, addr)

while True:
data = conn.recv(1024)
if len(data) == 0: #在Windows平台上,客户端断连会抛出异常
print('client is closed')
break
print('received data', data.decode('utf-8'))
conn.send(data.decode('utf-8').upper().encode('utf-8'))

conn.close()
server.close()

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import socket

client = socket.socket()
client.connect(('localhost', 6971))

while True:
data = input("please input >>")
if len(data) == 0:
continue
client.send(data.encode('utf-8'))
data = client.recv(1024)
print(data.decode('utf-8'))

client.close()

以上代码实现了一组Socket应用,服务端接收客户端请求,将客户端传过来的字符串转换为大写之后,再传回给客户端。先从整体看一下Socket服务端程序,首先创建一个socket对象,然后绑定IP地址和端口号,启动监听,接受客户端连接,然后开始接收数据。然后按照顺序看局部:

1. 创建socket对象 - socket.socket

socket.socket()函数,在参数为空时,使用默认的参数,该函数声明如下:

1
socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)

第一个参数为Socket Families(地址簇),常量名称以“AF_”打头,从官网和源码中可以看到很多的类型,我目前学习接触到的有“socket.AF_INET”和“socket.AF_INET6”,分别代表了IPV4和IPV6,其他的类型以后有用到再补充;第二个参数为Socket Types,当地址簇为“socket.AF_INET”和“socket.AF_INET6”时,常用的Socket Type有以下的几种:

Socket Type 说明
socket.SOCK_STREAM for tcp
socket.SOCK_DGRAM for udp
socket.SOCK_RAW 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。
socket.SOCK_RDM 是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。

2. 绑定地址和端口 - socket.bind

注意传入 socket.bind 函数的参数为一个元组,原因我推测是 socket.socket() 创建socket对象时,如果传入其他的Family和Type,socket.bind 函数的入参会有很大的差别,所以索性将 socket.bind 函数的入参定义为元组的形式。

server.bind((‘0.0.0.0’, 6971)) 表示绑定本机所有IP地址的6871端口,如果你的服务器配置又多个IP地址,而你想要限定对其中某个IP地址开启Socket服务,可以将 ‘0.0.0.0’ 替换为你需要开启Socket服务的IP地址。

3. 启动端口监听 - socket.listen

这个函数在不同的环境下有不同的效果,在Ubuntu 18.04 + Python3.6的环境执行以上程序,启动多个客户端,会有1个客户端与服务端建立起了连接并且可以交互,此外会有6个客户端阻塞在第10行 client.send 前,其他启动的客户端则阻塞在第4行 client.connect。如果将服务端 server.listen(5) 改为 server.listen(0),则会有1个客户端阻塞在第10行 client.send 前。

对此我是这样理解的,server.listen 函数定义了一个等待交互的客户端队列长度,队列的长度为参数 n + 1,已经与服务端建立了连接并且正在通信的client不在队列中。所以当 server.listen(5) 时,有6个客户端被Python解释器阻塞在了connect之后,send之前;其余的则被系统或者Python解释器阻塞在了connect一步。

文字表述比较难懂,建议在自己的环境上执行一下试试。

4. 接收客户端数据 - conn.recv

socket.accept 函数接收了一个客户端连接,返回一个 conn 连接对象,以及客户端地址。conn.recv 接收客户端发送过来的数据,conn.recv(1024) 的参数1024表示最多接收1024字节,可以将参数写大一些用于一次性接收更多的数据,但是一次性接收的数据不可能无限多,所以将这个参数配置得很大是没有意义的。一次最多接收多少数据,不同的环境有不同的限制。

5. 判断客户端是否断连

服务端程序第13行,我们通过 len(data) == 0 判断客户端是否断连,这个判断在Ubuntu系统上有效,在Windows系统中,如果客户端断连,服务端会抛出异常,需要对这个异常进行捕获。

6. 最后说说客户端

客户端的实现比较简单,首先建立连接,然后循环发送数据。注意第8行的判断,如果用户输入为空,continue 进入下一轮循环,原因是这样的:用户输入的数据为空时,客户端的确会通过 socket 发出一个空的数据,但是服务端收不到这个空的数据。

这样一来,客户端认为自己已经发送了,阻塞在11行 data = client.recv(1024) 等待服务端的响应,而服务端没有接收到任何数据,自然也不会发送响应,最后的结果就是程序卡死。为了规避该问题,我们在Socket发送数据前,先坚持即将发送的数据是否为空,如果将要发送的数据为空,则不再发送。

处理粘包

“粘包”通俗一点说,就是服务端发送了多个包,但是客户接收的时候,多个包粘在了一起,变成了一个包。粘包并不复杂,要明确首先粘包并没有丢包,所有的数据包都在缓冲区区里面;其次粘包并没有重包,没有一份数据在缓冲区里出现了多次,还需要你自己去重。

为什么会出现粘包,有下面两点原因: (转载自他人网页,链接 )

1、发送端需要等缓冲区满才发送出去,造成粘包 (发送端出现粘包)
2、接收端没有及时接收缓冲区包数据,造成一次性接收多个包,出现粘包 (接收端出现粘包)

理解了粘包的原理之后,处理起来并不困难,总结了一下处理Python的粘包有三种方法:

  1. 发送第一个包后,第二个包前sleep一段时间,导致缓冲区超时,这种处理方法的问题就是造成了程序的延时。
  2. 发送第一个包后,等到另一端确认接收完毕,再发送第二个数据包。
  3. 发送一个结构体,让另一端明确需要接收的数据包长度,因而只接收固定长度的数据包。

SocketServer

SocketServer是Python对简单socket进行的一个服务端的封装,本文最开始的示例中的Socket服务端程序,只能与单个客户端进行交互,可以使用 socketserver.ThreadingTCPServer 进行改写,用很少的代码,就能实现一个支持多线程,可以与多个客户端同时进行交互的Socket服务端程序。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import socketserver

class SimpleHandler(socketserver.BaseRequestHandler):
def handle(self):
while True:
data = self.request.recv(1024)
if len(data) == 0:
print('client is closed')
break
print('received data', data.decode())
self.request.send(data.decode().upper().encode())

if __name__ == '__main__':
server = socketserver.ThreadingTCPServer(('localhost', 6971), SimpleHandler)
server.serve_forever()

更为详细的SocketServer使用方法,在网上可以搜到很多,更建议的参考官网,带着具体的问题去寻找答案。

个人基于Python Socket写的一个Ftp程序:链接

谈兴趣

发表于 2019-07-07 | 更新于 2019-07-15 | 分类于 Diary

毫无疑问,如果一个人对某件事情拥有极高的兴趣,那么对他来说,别人眼中枯燥无味的事情,在他那里就变得充满乐趣,如果他(她)有幸能够一直做他感兴趣的事情,大概率的他能比其他人做得更好,甚至做出相当的成就。

然而兴趣并不是魔法,从古至今,有无数人对飞行充满了兴趣,但是飞机的发明,却是最近一两百年的事情。从对飞行的兴趣,到飞机的发明之间隔着什么呢,是科学技术的进步,是技术的积累。可见兴趣并不一定意味着成功,从兴趣到成功,首先是行动,其次往往有一些主观或者客观的苦难,需要付出努力去突破,有些困难可能终其个人的一生也突破不了。

很多人理解的兴趣,其实是对结果的憧憬,并不是真正的兴趣,比如很多人对创业充满了兴趣,但是他们感兴趣的,其实是创业成功后的辉煌,香车美女以及社会地位。对于结果的憧憬可以催人行动,但是因为兴趣并不在过程,一旦遭遇挫折,他(她)随时都可能放弃。所以人们常说,相比于雄心壮志,更为珍贵的是百折不挠的精神,有志者其实很多,能坚持到最后不违初心的人却很少。

吐槽一下Windows10上的中文输入法

发表于 2019-06-06 | 更新于 2019-06-11 | 分类于 Diary

这是一篇非常单纯的吐槽文章,以下的操作环境为Surface Windows10环境。

满打满算,我接触电脑到现在不到十年,高中时候的微机课不算。但是这短短几年的时间,也形成了自己的一些使用习惯,比如输入法,平时在Windows7上感觉不到,但是切了Windows10之后,微软拼音输入法不一样的使用习惯就让我很头疼了。首先,我来对自己的使用习惯做一个小结,相信很多Windows7一路过来的朋友,也有类似的习惯,在平时上网或者打字时,遇到英文输入,我习惯按一个Shift,将输入法切换为英文模式,敲完了英文字母之后再切回来;在写代码的时候,以英文输入为主,而且偶尔需要按着Shift键输入大写字母,输入单个大写字母时,我不习惯按Caps切换成大写模式,这个时候,如果不小心触摸到了Shift键,输入法被切换成了中文,也是一件比较不爽的事情,所以在写代码之前,我习惯用Ctrl+Space将输入法切换到英文键盘。这些按键习惯也已经基本固定了,最好不要让我尝试其他的按键组合。

总结一下,自己的需求和痛点:

  1. 电脑输入应该有两种模式,中文输入模式与英文输入模式,用Ctrl+Space切换
  2. 中文输入模式下,按Shift键可以输入英文字母,再按Shift键切换回中文输入
  3. 英文输入模式下,始终只能输入英文,单击Shift不要切换到中文

就这么简单的三点需求,Windows10差点把我搞吐血。首先我的Surface上默认安装了微软拼音输入法,这里不谈微软输入法输入的效率,仅仅关于一下上面提出的三个需求。首先微软输入法的中英文切换默认是Ctrl+Space,单击Shift无法输入英文字母,不满足需求2。可以修改设置,让单击Shift切换到英文输入,经过这样的设置之后,再按一次Shift便会切换回中文,没有专门的英文输入模式,不满足需求3。

有两种解决方法,第一是安装英文包,安装之后就拥有了一套英文的输入模式,可以在英文模式与微软输入法之间切换,但是切换的快捷键并不能配制成Ctrl+Space,也就是需求1没有满足。

第二个法子,通过安装一个插件,搜索一下Windows10输入法切换插件就能找到,开启该插件时,可以通过Ctrl+Space切换中英文模式,可以满足以上三点需求。但是在Surface使用过程中,又发现了新的问题,首先在切换到英文模式时,语言首选项栏显示的是“简体”,这就让我用起来很困惑了,明明是英文模式,哪怕你显示一个Eng,或者“英语”两个汉字也好,显示一个简体是什么意思。其次还有一个小问题,我不知道为啥,Windows10的语言首选项不能隐藏,即使通过设置隐藏相关图标和文字,重启电脑又显示出来了;而中英模式切换时,语言栏显示的内容不同,占据屏幕的宽度也不同,所以随着输入模式的切换,右下角的几个图标一会儿左移一下,一会儿右移一下,分散了我输入时的注意力。

图1:安装插件后的英文模式,“简体”两个汉字让人很奇怪

图2:安装插件后切换搜狗输入法,看起来还行

图3:安装插件后切换微软拼音输入法,注意右下角图标栏的宽度

这下我有点无语了,好不容易找到了两个法子,都不能完全解决我的问题,如此过了好几个月。

最后,终于到最后了,最后是重点,偶然发现了手心输入法,一个输入法,完美的满足了我的需求。不需要安装英文包,也不需要安装插件。它非常天才的再输入法里面集成了英文的输入模式,按Ctrl+Shift即可切换,与Windows的语言栏的集成也很好。而且它还非常天才的集成了一种没法输入的模式,在切换应用无法输入时,自动进入并显示没法输入模式,这点细节也让人感到贴心。手心输入法的四种模式请看下图:

图4:手心输入法中文模式

图5:手心输入法中文模式输入英文

图6:手心输入法英文模式

图7:手心输入法没法输入模式

本文绝非广告贴!手心输入法在兼容Window7用户的体验上真的做得不错,亲测搜狗输入法目前无法满足以上的3点需求。

Python学习总结01——基础数据类型

发表于 2019-05-03 | 更新于 2019-07-22 | 分类于 Python , Basic

写在前面的

Python学习总结这一系列博客,记录了自己在学习Python3的过程中的一些知识总结与思考。写博客出发点不为求多求全,主要记录下面几类知识:

  1. 容易忽略但是又很重要的知识,比如Python3的字符编码
  2. 理解起来比较困难的知识,比如metaclass
  3. 记忆起来比较困难的知识,这一点和上面一点又重合,但又不完全相同,比如Socket,理解起来很容易,但是写起来还是费劲的。记录这部分知识,自然是为了以后查找复制之用。

相应的,显而易见的,容易理解,容易记忆的知识,博客中不做记录。如果你有缘看到了这些博客,它们对你学习入门可能起不到太大的帮助,但是或许可以帮你查漏补缺,让我们一起思考与交流。

数值运算

Python相比与Java和C++,在加减乘除之外,多了两个运算符:// 和 **。抛开几个比较简单的运算符,这里关注下表中的几个运算符:

运算符 描述 实例
/ 除法运算 4 / 2得到2.0, 结果为float类型
// 整数运算 4 // 2得到2, 结果为int类型; 4.0 // 2得到2.0, 结果为float类型
** 幂运算 4 ** 2得到16, 结果为int类型; 4.0 ** 2得到16.0, 结果为float类型

注意Python的’//‘作用于int类型时可以相当于Java和C++的’/‘,而当参数之一为float类型时却与Java和C++的效果不同。简言之Python严格区分了除法和整除运算符,而Java和C++没有区分,Java和C++的’/‘作用于两个int时为整除,参数之一为浮点类型是为除法。

Python新增幂运算符**,那么问题来了, $\sqrt{4}$怎么写, 你可能已经猜到了:4 ** 0.5;相似的, $\sqrt[3]{4}$可以写作:4 ** (1 / 3)。

数值类型

Python3分别用int类型和float类型表示整数与浮点数,问题来了,Python3有没有long和double类型?答案是没有,接着往下问,int类型和float的表示范围分别是什么?

尽量不卖关子,Python3的int类型取代了旧的long类型,换句话说int类型没有范围限制,网上有些博客写到在Python3里面int的最大值是sys.maxsize, 这其实是误解,从名字就可以看出端倪,sys.maxsize表示的是list和str最大的size,并不是int类型的最大值。以下为官网的部分摘要:

PEP 0237: Essentially, long renamed to int. That is, there is only one built-in integral type, named int; but it behaves mostly like the old long type.

The sys.maxint constant was removed, since there is no longer a limit to the value of integers. However, sys.maxsize can be used as an integer larger than any practical list or string index. It conforms to the implementation’s “natural” integer size and is typically the same as sys.maxint in previous releases on the same platform (assuming the same build options).

与int不同,float类型确实有最大值的,可以通过sys.float_info.max和sys.float_info.min分别获取float的最大值和最小值。它们分别为:1.7976931348623157e+308 和 2.2250738585072014e-308,在Ubuntu环境上测试,Python的float类型的范围与C++的Double类型的范围相同,C++的Double类型的范围可以通过执行以下代码查看:

1
2
3
4
5
6
7
8
#include <iostream>
#include <limits>
using namespace std;
int main() {
cout << "double maxvalue" << numeric_limits<double>::max() << endl;
cout << "double minvalue" << numeric_limits<double>::min() << endl;
return 0;
}

Python3的float还包含了正无穷和负无穷的表示,float(“inf”)可以表示正无穷,float(“-inf”)可以表示负无穷。既然float能够表示的数据范围有限,Python怎么进行浮点型的大数计算呢,可以通过Decimal实现,需要import引入decimal模块,不再这里赘述。

float不光有数据范围的限制,float之间的计算还需要考虑精度的损失,两个浮点数据之间不能直接比较。sys.float_info.epsilon可以表示机器能够区分出的两个浮点数的最小差别,判断两个函数是否相等,可以定义函数如下:

1
2
def is_float_equal(a,  b):
return abs(a - b) <= sys.float_info.epsilon

此外,还可以通过import math模块,通过math.isclose(a, b, *, rel_tol=1e-09, abs_tol=0.0)函数判断两个浮点数是否相等,按照官网描述,该函数实现逻辑如下:

1
return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)

稍加留心,我们可以注意到math.isclose函数默认比较参数间的相对误差,可以使用match.isclose重写上文的is_float_equal函数,比较两个浮点数据的绝对误差:

1
math.isclose(a, b, rel_tol=0, abs_tol=sys.float_info.epsilon)

列表 - list

Python用列表list表示了Java和C++中的Array,List和Vector等类型,这确实省去了很多的麻烦,接下来从“增删改查”看看列表的操作。

1.“增”,下表分为从列表的末尾或者中间,添加列表或者单个元素:

操作 函数实现 切片实现
在my_list末尾插入列表 my_list.extent(another_list) my_list[len(my_list):]=another_list
在my_list末尾插入元素 my_list.append(element) my_list[len(my_list):]=[element,]
在my_list的index下标位置插入列表 —- my_list[index:index] = another_list
在my_list的index下标位置插入元素 my_list.insert(index, element) my_list[index:index]=[element,]

不由得感叹,切片真是一个无比神奇的东西!注意通过切片或者extent函数插入字符串str时,会将str作为列表处理,我们看下面一段代码:

1
2
3
my_list = [1, 2, 3]
my_list.extent('123')
print(my_list)

它的执行结果是:1,2,3,’1’,’2’,’3’,而不是:1,2,3,’1,2,3’。

2.“删”,下表分别从列表的末尾或者中间删除元素

操作 函数实现 切片实现
删除my_list末尾的元素 my_list.pop() my_list[-1:0]=[]
删除my_list下标为index的元素 del my_list[index] my_list[index:index+1]=[]

一般不会遇到从列表中删除列表这种需求,如果遇到了,也可以使用切片实现,比如保留my_list中前三个元素,其他全部删除:my_list[3:] = []

3.“改”

更改列表中的元素,最自然而然的,就是使用下标操作,或者切片,不再赘述。

4.“查”

说到查,最自然而然的,是想到find关键字,然而很遗憾,Python并没有find函数,因为它有另一个关键字:in,或者我们可以转变一下思维,从列表my_list中查找element元素,可以转而判断my_list中element元素的个数count是否大于0,或者求element元素在my_list中的下标index。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
my_list = [1,2,3]
element = 1
#判断element是否在my_list中
if element in my_list:
print("exist")
#或者
if my_list.count(element) > 0:
print("exist")
#或者下面这样
try:
my_list.index(element)
except ValueError:
print('not exist')
else:
print('exist')

列表的index函数在这里略显诡异,如果element在my_list中不存在,它并没有返回-1或者其他的无效的值,而是直接抛出异常。我觉得这又是Python思想的一个体现,解决一个问题一种实现就足够了,第二种第三种实现都是多余的,用index函数判断元素是否存在就是多余的,不被推荐的实现。

元组 - tuple

“元组(tuple)”可以被看做不能被修改的列表,因为不能被修改,元组可以作为字典的key,而列表不行。当一个函数包含了多个返回值时,使用type命令查看该函数的返回值,可以看到其类型是一个元组。

字符串 - str

一些思考

以下内容仅代表个人观点和思考,可能有失偏颇。

先讨论一点,int 和 float 类型,即整类型和浮点数类型,是不是类,如果你和我一样,是从C++和Java走来的,那我想我们应该有相似的认识,int 和 float 属于基本数据类型,所谓基本数据类型,就是它们不是类。但是毫无疑问,在Python的世界里面,int 和 float 都是类,不信你用 type 函数测试一下,Python会清晰的告诉你,它们的类型分别是和。

那么问题来了,你说它是类,它就是类了吗?这个问题我自己思考了比较久,我自己的看法是,还是别把 int 和 float 基本数据类型当类。看到这里可能有人已经在心里骂我了,我们先想想什么是类,面向对象的忠实信徒可能会说,万事万物皆可为类,可是回想一下类的定义:类是对现实生活中一类具有共同特征的事物的抽象。你是否接受有些东西不可抽象,数字是什么的抽象,既然是抽象,意味着有些特征可以忽略,当我们说数字是类,我们是需要使用它什么特征,又需要忽略它什么特性。我觉得这个问题说不清楚,所以我宁愿接受 int 不是类,当然对这个问题你可能有自己的观点。

为什么我要花时间扯这些问题呢,因为我觉得我是个笨人,我这个笨人有这样一些特点,如果一些东西分类不够明确,我就可能用混;如果有些东西说不清楚它是什么,除非每天都接触,否则我就会忘。

当我从数字类型本身,而不是类的角度看 int 和 float 时,就多了一个看它的维度,首先是它具备了原子性,不能对它内部进行修改,从这一点出发,Python世界里的 str 和 tuple 类型有相似的特点,而C++世界里的std::string类型则没有;其次它们再次变得简单,我可以用小学所学的加减乘除作用于它,不需要关注更加复杂的东西。

3. Python3的列表拷贝

引用赋值,浅拷贝,深拷贝

实现类的成员拷贝函数?留到后面,我还没有学到

Ubuntu安装使用MariaDB

发表于 2019-03-31 | 更新于 2019-05-03 | 分类于 Database , Maintenance

在Ubuntu上安装MariaDB的经历,其挫折系数仅次于数年前安装Oracle。然而安装完成之后再回头看,其实也并没有那么复杂,搞出这么多幺蛾子,还是自己因为自己对MySQL和MariaDB一知半解,吃了没文化的亏。

MadiaDB数据库是MySQL数据库的一个非常活跃的分支,为什么要安装MariaDB,因为MySQL被Oracle公司收购之后,更新节奏变慢,而且有被Oracle闭源的风险。所以,是时候跟着MySQL之父Michael Widenius一起拥抱MariaDB了。

我的Ubuntu系统为Ubuntu 18.04 LTS,安装的MariaDB版本为10.3版本,下面给出安装命令:

1
2
3
4
5
6
7
8
9
#安装依赖包
apt install software-properties-common -y
#添加密钥
apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xF1656F24C74CD1D8
#添加PPA源
add-apt-repository 'deb [arch=amd64] http://mirrors.tuna.tsinghua.edu.cn/mariadb/repo/10.3/ubuntu bionic main'
#安装mariadb-server
apt update
apt install mariadb-server -y

如果你想要安装10.2, 10.1, 5.5等其他版本的MariaDB,请将命令中的10.3替换为对应的版本号

怎么样,是不是很简单,那我怎么就掉进了坑里了?怎么掉进去的我也不知道,当我重启电脑之后,发现MariaDB数据库没起来。我尝试用“systemctl start mysql”命令启动MariaDB,启动了一段时间之后启动失败了,根据提示使用“journalctl -xe”查看错误日志,发现日志中如下的错误:

1
kernel: [ 2336.792423] audit: type=1400 audit(1470265086.730:518): apparmor="DENIED" operation="sendmsg" info="Failed name lookup - disconnected path" error=-13 profile="/usr/sbin/mysqld" name="run/systemd/notify" pid=11850 comm="mysqld" requested_mask="w" denied_mask="w" fsuid=117 ouid=0

从日志里面看,MariaDB启动失败和apparmor有关,apparmor是什么东西,遇到这个问题之前我不懂,因为这个问题我简单了解了一下,apparmor安全策略用于定义个别程序可以访问的系统资源以及各自的特权,简单理解一下,是一个做安全配置的东西。当我根据网上搜来的一些指导去编辑MySQL的安全配置文件“/etc/apparmor.d/usr.sbin.mysqld”时,我注意到文件内容是空的,仅有如下一段注释说明:

# This file is intentionally empty to disable apparmor by default for newer
# versions of MariaDB, while providing seamless upgrade from older versions
# and from mysql, where apparmor is used.
#
# By default, we do not want to have any apparmor profile for the MariaDB
# server. It does not provide much useful functionality/security, and causes
# several problems for users who often are not even aware that apparmor
# exists and runs on their system.
#
# Users can modify and maintain their own profile, and in this case it will
# be used.
#
# When upgrading from previous version, users who modified the profile
# will be prompted to keep or discard it, while for default installs
# we will automatically disable the profile.

注意上文中被我加粗的内容,MariaDB默认是关闭了apparmor策略的,那为什么还是被apparmor搞得启都启不来了,一脸懵逼。随后我继续尝试搜索解决方案,按照网上的一些教程对apparmor配置文件进行了配置,取得了一些进展,不过仅限于日志中的错误信息发生了变化。

最后还是在stackoverflow中找到了解决方法,解决方法还是关闭MariaDB的apparmor配置,使用下面的方法:

1
2
3
#Type this on your host terminal
sudo ln -s /etc/apparmor.d/usr.sbin.mysqld /etc/apparmor.d/disable/
sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.mysqld

在博客园的一篇博客中,找到了对上述操作的解释,/etc/apparmor.d/disable目录可以和apparmor_parser -R选项一起使用以禁用一个配置文件,这里给出链接。

写这篇博客时,我尝试将它写得悲壮一点,像读者传递一下解决问题过程中的艰辛,但是写博客的过程中,我渐渐放弃了这个想法,还是写得朴实一点,把事情写清楚最重要。对于我们大多数程序猿来说,解决问题就是我们的日常,每当解决了一个问题,就积累了一些经验,所以面对问题,我们应该抱有积极的心态。在解决这个问题的过程中,我将MariaDB翻来覆去安装了很多遍,配置文件改了又改,在MySQL的配置,以及日志的查看等方面有学到了一些知识。

最后,在解决MariaDB安装的过程中,还有一个意外的收获,解决了Ubuntu上网易云音乐每次启动都需要重新登录的问题,原因就在“.cache/netease-cloud-music/”目录下有一些文件的属主是root,用“chown -R”命令修改整个目录的属主为自己的用户之后,问题就解决了!

123
Leon

Leon

君子励锋芒之剑
隐而不发
21 日志
6 分类
4 标签
GitHub E-Mail
© 2019 Leon
由 Hexo 强力驱动 v3.9.0
|
主题 – NexT.Mist v7.0.1