计算机组成原理
计算机的基本组成如图
- 内核暂时先理解成系统程序,比如我们想通过键盘获取到用户的输入,想打开网卡录取视频。这些硬件是受系统保护的,只能交给内核控制。不可能把控制权交给用户程序。
- 用户程序如果想访问硬件,只能用户调用内核暴露的一些调用,我们称这个为系统调用。
- 操作系统启动的时候,会把内核程序所在的地址空间设为绝对安全的空间,这个空间称为内核空间,这种机制称为保护模式。其他的空间 即提供给用户程序使用,称为用户空间。比如JVM,QQ,微信对内核来说都是用户App。
- 操作系统启动后,OS会在一个叫做GDT(全局描述符表)的表里标识出内核空间的位置。
中断 晶振
假如单核CPU上安装一个操作系统,系统运行了多个程序,包含内核程序和用户程序;此时在一个瞬间CPU只能执行一个程序
- 晶振1秒震动成千上万次,没震动一次,就会传递一个时钟中断给cpu,假如当前运行QQ,晶振震动之后,CPU会把APP1的数据从CPU的高速寄存器中缓存到这个用户的程序空间中。
- OS启动的时,会有一个中断向量表(中断表述符表IDT(Interrupt Descriptor Table)),OS启动时,内核程序注册的。
- 中断产生后,存在一个中断进程调度。这个调用程序逻辑代码是由内核注册的。
当时钟中断产生后,CPU把现场保存了,CPU本不知道下一步要干什么。
但是在OS启动的时候,内核已经注册了中断向量表,
它CPU先保护现场,然后来调用我内核程序中的一个回调程序逻辑代码
总结:
- CPU的保护现场与恢复现场,会使CPU高速寄存器和内存之间的数据传递,会有消耗
- CPU进行系统调用"进程调度"的次数会有消耗
- 程序运行的越多,单位时间内,CPU浪费在内核调度上的时间会变多,浪费在寄存器与内存数据传递的时间会变多,真正运行程序的时间会变少
BIO(Blocking IO)
代码示例
public class SocketBIO {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9090,10);
System.out.println("step1: new ServerSocket(9090,10)");
while(true){
Socket client = serverSocket.accept();
System.out.println("step2:client->"+client.getPort());
new Thread(new Runnable() {
Socket socket;
public Runnable setSocket(Socket s){
socket = s;
return this;
}
@Override
public void run() {
try {
InputStream in = client.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
while (true){
String dataLine = reader.readLine();
if(null != dataLine){
System.out.println("dataLine==>"+dataLine);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.setSocket(client)).start();
}
}
}
strace
将代码拷贝搭配Linux环境下利用strace命令跟踪Java BIO
strace -ff -o out java SocketBIO
step1 new ServerSocket(8090)
此时服务器已经监听来自客户端的连接,由于没有任何连接建立,所有打印==step1 new ServerSocket(8090) ==就阻塞在这儿 关键几行代码
vi out.1521
...
2527 socket(PF_INET6, SOCK_STREAM, IPPROTO_IP) = 3
2528 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
2529 bind(3, {sa_family=AF_INET6, sin6_port=htons(9090), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 24) = 0
2530 listen(3, 50)
信息打印阻塞 红框内表示任何IP及端口都可以连接本地9090端口 连接测试
nc localhost 9090
继续分析关键几行
2455 accept(5, {sa_family=AF_INET6, sin6_port=htons(40368), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 6
...
2472 clone(child_stack=0x7f584026cff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CL
2472 ONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f584026d9d0, tls=0x7f584026d700, child_tidp
2472 tr=0x7f584026d9d0) = 6333
...
2476 accept(5,
2455行之前被阻塞在accept(5, 这时,当客户端连接时候,系统调用被唤醒,然后由于代码是新建new了一个线程去接收客户端,所以看到一个clone命令 2472行就是clone命令,代表的是创建一个线程,去处理这个客户端的输入。 2476行就是继续阻塞,去接受新客户端的连接。
发生信息测试 总结 BIO模式下每个连接占一个线程,cup单位时间内,每一个线程轮询一下; 除了要跑进程的逻辑,还要有一系列其他内核操作,比较浪费cup资源,因为进程(程序内线程太多了)
NIO (Nonblocking IO)
代码示例
public class SocketNIO {
public static void main(String[] args) throws Exception {
List<SocketChannel> clients = new LinkedList();
ServerSocketChannel channel = ServerSocketChannel.open();
channel.bind(new InetSocketAddress(9090));
channel.configureBlocking(false);
while(true){
Thread.sleep(1000);
SocketChannel client = channel.accept();
if(client == null){
System.out.println("null...");
}else{
client.configureBlocking(false);
int port = client.socket().getPort();
System.out.println("client port is:"+port);
clients.add(client);
}
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4096);
for (SocketChannel c:clients){
int num = c.read(byteBuffer);
if(num>0){
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
String str = new String(bytes);
System.out.println(c.socket().getPort()+":"+str);
byteBuffer.clear();
}
}
}
}
}
同样安装BIO的方式进行strace测试验证 发现:
- 没有客户端连接进来返回 -1;java返回null
- 有客户端连接,返回对应客户端的fd
- 假设有1万个连接但是有效调用只有一个,浪费资源
底层调用伪代码
socket()=3
bind(3, 9090)
listen(3)
while(true) {
accept(3)
5.nonBlocking = true;
for(Client client : allClient) {
read(client);
}
}
总结: 优点:程序自己变了一大堆不受一个没有数据的socket而阻塞 缺点:每一个socket都会触发一个用户状态用内核的系统调用
|