一、forking
1.什么是forking
fork(分岔),当执行一个命令时,父进程(当前进程)fork出一个子进程
父进程将自身资源拷贝一份,命令在子进程中运行时,就具有和父进程完全一样的运行环境
2.进程的生命周期
父进程fork出子进程并挂起
子进程运行完毕后,释放大部分资源并通知父进程,这个时候,子进程被称作僵尸进程,父进程获知进程结束,子进程所有资源被释放
3.僵尸进程
僵尸进程没有任何可执行代码,也不能被调度,如果系统中存在过多的僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,对于系统管理员来说,试图杀死其父进程或重启系统来消除僵尸进程
4.forking编程基本思路
需要用到os模块,os.fork()函数实现forking功能,在pthon中,绝大多数的函数只返回一次,os.fork()返回两次。对fork()调用,针对父进程返回子进程的PID(>0),对于子进程,返回PID=0
二、forking小实验--扫描存活主机
要求:
编写一个myfork.py脚本,实现以下功能:
- 在父进程中打印“In parent”然后睡眠10秒
- 在子进程中编写循环,循环5次,输出当前系统时间,每次循环结束后睡眠1秒
- 父子进程结束后,分别打印“parent exit”和“child exit”
方案:
子进程运行时是从 pid = os.fork() 下面语句执行,实际上,该语句是两条语句, os.frok() 是创建子进程语句,而 pid = 是赋值语句,所以在创建完子进程后,下一句为运行赋值语句。
- 进程调用fork函数时,操作系统会新建一个子进程,它本质上与父进程完全相同。操作系统是将当前的进程(父进程)复制了一份(子进程),然后分别在父进程和子进程内返回。子进程接收返回值为0,此时pid=0,而父进程接收子进程的pid作为返回值。调用fork函数后,两个进程并发执行同一个程序,首先执行的是调用了fork之后的下一行代码。
- 此时,pid两个值,同时满足判断语句if和else,按照顺序执行如下:
- 父进程先执行:程序先输出“In parent!”,然后父进程睡眠10s,即进程挂起10s
- 父进程挂起时,子进程开始执行:循环5次,每循环一次打印当前时间后睡眠1s,5s后结束五次循环,打印“child exit”,此时子进程已经结束
- 子进程接收后,父进程挂起尚未结束,当父进程睡眠时间结束后,打印“parent exit”,父进程也结束了
- [root@localhost day09]# vim myfork.py
- #!/usr/bin/env python3
- import os
- import time
- from datetime import datetime
- pid = os.fork()
- if pid:
- print("In parent!")
- time.sleep(10)
- print("parent exit")
- else:
- for i in range(5):
- print(datetime.now())
- time.sleep(1)
- print("child exit")
测试脚本执行
- [root@localhost day09]# python3 myfork.py
- In parent!
- 2018-09-03 10:48:46.552528
- 2018-09-03 10:48:47.553714
- 2018-09-03 10:48:48.554800
- 2018-09-03 10:48:49.555901
- 2018-09-03 10:48:50.557035
- child exit
- parent exit
三、僵尸进程zombie问题探讨
父进程通过os.wait()来得到子进程是否终止的信息,在子进程终止和父进程调用wait()之间的这段时间,子进程被称为zombie(僵尸)进程。
如果子进程还没有终止,父进程退出了,那么子进程会持续工作。系统自动将子进程的父进程设置为init进程,init将来负责清理僵尸进程
python可以使用waitpid()来处理子进程
waitpid()接受两个参数,第一个参数设置为-1,表示与wait()函数相同,第二个参数如果设置为0表示挂起父进程,直到子进程退出,设置为1表示不挂起父进程
waitpid()的返回值:如果子进程尚未结束则返回0
#!/usr/bin/env python3
import os,time
def reap():
result=os.waitpid(-1,1)
print('Reaped child process %d' % result[0])pid=os.fork()
if pid:
print('In parent.Sleeping 15s..')
time.sleep(15)
reap()
print('parent done')
else:
print('In child.Sleeping 5s..')
time.sleep(5)
print('Child termingating.')
执行结果:
[root@room9pc01 mnt]# python3 zombie.py
In parent.Sleeping 15s..
In child.Sleeping 5s..
Child termingating.
Reaped child process 19248
parent done
结果分析:
这个实验是为了证明子进程是否成为僵尸进程,根据waitpid()的返回值:如果子进程尚未结束则返回0,也就是说如果子进程死亡,则会返回子进程pid。
四、多线程编程
1.多线程的成因
多线程(MT)编程出现之前,电脑程序的运行由一个执行序列组成,执行序列按顺序在主机的中央处理器中运行,无论是任务本身要求顺序执行还是整个程序由多个子任务组成,程序都是按这种方式执行的,即使子任务相互独立,互相无关。如果并行运行这些相互独立的子任务则可以大幅度提高整个任务的效率
2.多线程任务的工作特点
多线程本质上是异步的,需要有硬件支持,例如我们常说的CPU多核多线程
各个事务的运行顺序是不确定的,随机的,不可预测的
编程任务被分为多个执行流,每个流都有自己的任务,并且完成他们自己的目标,根据这些应用的不同,这些子任务需要计算出一个中间结果,用于合并得到最后的结果
3.什么是进程
计算机程序不过是磁盘中可执行的、二进制(或类似)的数据,程序时静态的
进程是重量级的,是程序的一次执行过程,所以所进程是动态的
每个进程都有自己的地址空间、内存以及其他记录其运行轨迹的辅助数据
操作系统管理在其上运行的所有进程,并为这些进程公平低分配时间
4.什么是线程
线程是轻量级的,是程序的一次执行过程,所以所线程程是动态的,与进程类似。
不同的是,所有的线程运行在同一个进程中,共享相同的运行环境
一个进程的各个线程之间可共享同一片数据空间,所以比进程之间更方便地共享数据以及相互通讯
5.多线程编程
thread和treading模块允许程序员创建和管理线程
thread模块提供了基本的线程和锁的支持,而threading提供了更改级别,功能更强的线程管理功能
我们一般使用threading模块
6.传递函数给Thread类
多线程编程有多种方法,传递函数给threading模块的Thread类是第一种方法
Thread对象使用start()方法开始线程的执行,使用join()方法挂起程序,知道线程结束
7.传递给可调用类给Thread类
传递可调用类给Thead是第二种多线程编程方法
相对一个或几个函数来说,由于类对象里可以使用类的强大的功能,可以保持更多的信息,这种方法更为灵活
五、小实验2--扫描存活主机
需求:
我们可以写一个python脚本,扫描局域网所有的存活主机,如果懂ip协议的话,可以知道局域网的ip有其专用地址,如下
IPv4专用地址如下:
Class A 10.0.0.0-10.255.255.255
默认子网掩码:255.0.0.0
Class B 172.16.0.0-172.31.255.255
默认子网掩码:255.240.0.0
Class C 192.168.0.0-192.168.255.255
默认子网掩码:255.255.0.0
这里只举例ping一个网段,读者可以尝试用多个循环去完成实验
要求:
创建mtping.py脚本,实现以下功能:
- 通过ping测试主机是否可达
- 如果ping不通,不管什么原因都认为主机不可用
- 通过多线程方式实现并发扫描
方案:
- subprocess.call ()方法可以调用系统命令,其返回值是系统命令退出码,也就是如果系统命令成功执行,返回0,如果没有成功执行,返回非零值。
- 调用Ping对象,可以调用系统的ping命令,通过退出码来判断是否ping通了该主机。如果顺序执行,每个ping操作需要消耗数秒钟,全部的254个地址需要10分钟以上。而采用多线程,可以实现对这254个地址同时执行ping操作,并发的结果就是将执行时间缩短到了10秒钟左右。
实现此案例需要按照如下步骤进行。
步骤一:编写脚本
- [root@localhost day09]# vim mtping.py
- #!/usr/bin/env python3
- import subprocess
- import threading
- def ping(host):
- rc = subprocess.call(
- 'ping -c2 %s &> /dev/null' % host,
- shell=True
- )
- if rc:
- print('%s: down' % host)
- else:
- print('%s: up' % host)
- if __name__ == '__main__':
- ips = ['172.40.58.%s' % i for i in range(1, 255)]
- for ip in ips:
- t = threading.Thread(target=ping, args=(ip,)) # 创建线程,ping是上面定义的函数, args是传给ping函数的参数
- t.start() # 执行ping(ip)
面向对象代码编写方式如下:
- 定义Ping类,该类可实现允许ping通任何主机功能:
- 1.利用__init__方法初始化参数,当调用Ping类实例时,该方法自动调用
- 2. 利用__call__()方法让Ping类实例变成一个可调用对象调用,调用t.start()时, 引用subprocess模块执行shell命令ping所有主机,将执行结果返回给rc变量,此时,如果ping不通返回结果为1,如果能ping通返回结果为0
- 3.如果rc变量值不为0,表示ping不通,输出down
- 4.否则,表示可以ping通,输出up
- 利用列表推导式生成整个网段的IP地址列表[172.40.58.1,172.40.58.2....]
- 循环遍历整个网段列表,直接利用 Thread 类来创建线程对象,执行Ping(ip)。
- [root@localhost day09]# vim mtping2.py
- #!/usr/bin/env python3
- import threading
- import subprocess
- class Ping:
- def __init__(self, host):
- self.host = host
- def __call__(self):
- rc = subprocess.call(
- 'ping -c2 %s &> /dev/null' % self.host,
- shell=True
- )
- if rc:
- print('%s: down' % self.host)
- else:
- print('%s: up' % self.host)
- if __name__ == '__main__':
- ips = ('172.40.58.%s' % i for i in range(1, 255)) # 创建生成器
- for ip in ips:
- # 创建线程,Ping是上面定义的函数
- t = threading.Thread(target=Ping(ip)) # 创建Ping的实例target=Ping(ip)实例化对象
- t.start() #执行Ping(ip)
步骤二:测试脚本执行
- [root@localhost day09]# python3 udp_time_serv.py
- 172.40.58.1: up
- 172.40.58.69: up
- 172.40.58.87: up
- 172.40.58.90: up
- 172.40.58.102: up
- 172.40.58.101: up
- 172.40.58.105: up
- 172.40.58.106: up
- 172.40.58.108: up
- 172.40.58.110: up
- 172.40.58.109: up
- ...
- ...
- ...
- ...
- 172.40.58.241: down
- 172.40.58.242: down
- 172.40.58.243: down
- 172.40.58.245: down
- 172.40.58.246: down
- 172.40.58.248: down
- 172.40.58.247: down
- 172.40.58.250: down
- 172.40.58.249: down
- 172.40.58.251: down
- 172.40.58.252: down
- 172.40.58.253: down
- 172.40.58.254: down
总结:
以上提供了两种编程思路,一个是传递可调用类给Thread类,第二个是传递可调用函数给Thead类,推荐使用第一种方法。