一、操作系统中IO的交互
我们开发的服务就是发送数据和接收数据并处理,所以无时无刻软件开发者都在和IO打交道,而且基本上在和tcp或者udp打交道,要么就是应用层的http/https或者其他流媒体传输协议,要么就是socket通信。不管你使用什么通信协议,最终都是IO,我们上层的应用(用户空间)都是通过线程在内核空间运行时候执行指令将内控存储空间的IO数据读取到用户空间(存储位置)。网卡模块或者磁盘模块底层的驱动在和操作系统交互的时候,是依靠IO缓存区buffer,IO有数据过来驱动就会放到缓存区,然后通过中断事件告知系统内核去copy数据,这时候内核就会告知对应的线程去读buffer数据,然后清理buffer之后,再去读。当整个网络IO或者磁盘IO读取结束的时候,系统内核也会告知对应的线程去拿数据,要是读取磁盘文件,磁盘驱动会告知文件读取结束了;要是网卡读取数据,网卡会告知系统最后的tcp/udp包结束。所以可见,IO本来就是异步的(网卡驱动和内核之间),只是运行在内核中的线程是在等待(wait)IO事件回来去读数据。
二、用户线程的切变与jvm线程封装
上面有讲述线程运行在内核空间与切回到用户态(用户空间)的概念,这里需要着重讲解下。我们无论写的代码运行在windows还是在linux上或是ios上,我们程序的代码都是调用pthread去创建用户线程。当然我们使用的是高级开发语言,高级开发语言开发的程序有借助运行的虚拟机或者经过编译器编译之后都是本质调用系统的kthread. 这里举一个例子,对于java语言有中间运行的jvm,而且上面的资源全都是jdk封装的,如容器,内存的管理(堆栈),还有线程。对于线程的封装,早期的jvm是在用户存储空间创建一个用户线程,可能多个这样的线程对象在内核中对应一个kthread.这就是早期的M*N模型。
后来jvm让用户程序进程中的线程对象和创建的kthread一一对应起来,也就是jdk中thread其实封装的就是lwp.create().
一个用户空间的lwp线程被创建了,而且内核中有对应的内核线程,为啥还需要有线程的用户态和内核态的切换?这个问题需要被理解,才能了解线程不同态切换的缘由是什么,首先lwp是执行基本的运算操作,当用户程序仅仅是简单的计算处理,那么操作系统依靠用户态的调度器完全可以实现。但是lwp不对物理资源拥有权力,而且线程的协同也是需要内核协助的,这些操作都需要调用到内核线程去处理,那么就需要切换到内核态。
内核态权限大可以复制IO读写,网卡,内存等操作,用户态只能操作应用程序分配的空间
当需要进行高权限操作的时候,就需要从用户态切到内核态,比如,要读取文件,就需要从用户态切换到内核态(通过调用系统函数),进行文件读写,读写完成后再切换回来(普通IO时如果文件过大会进行多次用户和内核态的切换)
- 首先,大多数LWP的操作,如建立、析构以及同步,都需要进行系统调用。系统调用的代价相对较高:需要在user mode和kernel mode中切换。
- 每个LWP都需要有一个内核线程支持,因此LWP要消耗内核资源(内核线程的栈空间)。因此一个系统不能支持大量的LWP。
操作系统通过CS:IP来查找执行命令,其中CS的最低两位来表示内核和用户态(0:表示内核态,3:表示用户态,刚好符合inter权限)
切换时需要:
- 保留用户态现场(上下文、寄存器、用户栈等)
- 复制用户态参数,用户栈切到内核栈,进入内核态
- 额外的检查(因为内核代码对用户不信任)
- 执行内核态代码
- 复制内核态代码执行结果,回到用户态
- 恢复用户态现场(上下文、寄存器、用户栈等)
-
JDK 1.8 Thread.java 中 Thread#start 方法的实现,实际上是通过 Native 调用 start0 方法实现的;在 Linux 下, JVM Thread 的实现是基于 pthread_create 实现的,而 pthread_create 实际上是调用了 clone() 完成系统调用创建线程的。
-
所以,目前 Java 在 Linux 操作系统下采用的是用户线程加轻量级线程,一个用户线程映射到一个内核线程,即 1:1 线程模型。由于线程是通过内核调度,从一个线程切换到另一个线程就涉及到了上下文切换(对线程的操作也会进行用户和内核态的切换,如thread.yeld())
-
Linux的线程模型是1:1模型,而 Go 语言是使用了 N:M 线程模型实现了自己的调度器,它在 N 个内核线程上多路复用(或调度)M 个协程,协程的上下文切换是在用户态由协程调度器完成的,因此不需要陷入内核,相比之下,这个代价就很小了。
三、中断的理解
基于上述讲的IO驱动块和内核的交互本身就是异步(磁盘或者udp/tcp驱动)的,那么在内核这一块封装的方法有有两种方法。每读取回来的块数据readBuffer(),内核都会交给线程,但是线程收到数据去将数据存储,当时必须等到数据全部读取结束的时候再去做其他的事情。伪代码如下:
thread(){
status = readBuffer(data)
if(status == "finish"){
listData = listData + data
sendData(listData)
}else{
listData = listData + data
}
}
那么对于IO,(分为五种NIO,BIO,AIO,多路复用IO,事件驱动IO),BIO就是Blocking IO,就是用户线程一直处于阻塞状态,就是一直在等待内核线程的响应,这是怎么实现的呢?用户线程调用了系统内核线程,也就是切换到了内核态,且用户线程LWP处于waiting状态。
在这里必须对操作系统做一个线程知识上的介绍:cpu指令集吧操作系统的级别划分四个等级,分别是3,2,1,0;级别也是逐级升高,3的级别最低,没有办法操作底层io,cpu等资源。所以3级别的线程称作用户线程,也是在用户空间内操作存储空间。对于用户空间也有一个线程调度器,用来分配用户线程轻量级的进程LWP,然后用户线程通过LWP与内核线程交互(用户线程和LWP之间有一定的比例关系可以是1:1,也可以是M:N)。
上述所说的用户线程调用系统方法进行IO端口监听或者读写操作都会被调度器置于waiting,等待LWP接收到内核线程方法事件回来才能切换waiting去执行下面的指令操作。那么Non-Blocking IO就是在调用系统方法的时候,系统方法直接先返回一个状态,这个状态意思就是系统内核线程去操作IO了,LWP等会再去询问结果,那么用户线程需要一直循环访问内核线程返回的状态结果,这个结果是内核线程将数据集写到用户空间。那么内核线程是如何操作IO的,以及如何和网卡的中断进行交互的?
上图大致说明了IO与内核线程的交互过程,首先是IO触发网卡的中断,然后告知系统内核,内核收到中断调度对应的内核线程,内核线程根据中断事件的信息识别对应数据句柄信息。如果是IO磁盘文件那么有对应的文件描述数据,如果是网络IO数据呢?
中断事件发生可以通知系统调度器激活对应的线程,或者调度器重新启动线程执行响应的指令,但是需要提前设置好指令的函数指针,也就是回调的函数入口;当然这个回调的函数入口也可以是激活的内核线程去执行。对于比较底层的中断,调度器实现的其实本质是中断指针入口切换,就像单片开发,定时器与外部中断,配置中断入口函数,在单片执行执行的就是一个顺序的指令路线,中断来了,就会切入到中断入口函数。之前的操作会被保存起来,执行完中断之后,再被切换回来。
四、网络编程
使用每一个独立的线程处理连接并读取请求数据,然后进行业务数据处理。这样方式使得每一个连接能够及时响应, 这就是NIO实现的最早雏形。
main(){
client = socket();
serv = client.accept();
listClient.add(serv);
new thread{
req = serv.readBufferData()
parse(req)
...
}.start()
}
NIO有两个问题
1)线程一直waiting,空耗时间片分配,也就是空耗cpu资源
2)就是线程都在阻塞中,不做任何处理
其实NIO可以做很多优化,不让那么线程去空耗,使用一个线程去空耗,然后激活任务线程去处理:
NIO就是非阻塞IO,操作IO的线程处于什么状态,处于休眠状态吗?
什么是异步?异步肯定是两个线程,其中一个线程提交了任务,并提交回调函数的指针;那么当事件触发了,就会触发新的线程,或者就在触发线程中执行回调方法。一个线程肯定是顺序执行,那么就是终止,要么就是继续执行,没有异步而言。
事件驱动:1)我们见到的软件交互中,最常见就是ui线程,其中支持异步操作的就是其他线程向其Looper()消息队列中添加消息。这也是线程异步通信很重要的方式,线程维护了消息队列,让其他线程申请拿到消息队列,并塞入数据。2)事件由硬件产生,并激活线程。
其实上述说的ui线程那是对用户空间而言的,对于内核而言没有ui线程而言,只是线程的级别高了而已。ui线程消息队列looper, 说白了那是用户空间层面实现的,说白了依据looper和ui线程传递数据,还是在于内存空间的共享实现的。其他子线程通过拿到looper对象,对其操作。内核线程之间的通信也就是信号量,信号量是由内核的调度器来处理和分发的,PCB依据信号量对线程有进一步的操作。
对于IO系统内核进行早期的优化就是多路复用的方式,一个负责监听内核线程对应多了连接并放在列表中,就是IO事件触发之后,内核线程激活之后,会有某一个的连接文件句柄信息匹配,将文件写到文件中。然后用户进程去遍历连接的文件句柄队列,这也就是selector做的事情,然后输出给对应的通道,业务逻辑处理了通道中请求的数据,然后写数据返回给客户端。连接通道的匹配是依托selector实现,每一个连接都会有连接句柄信息,如果是文件读取,那么会有文件句柄信息,如果是网络IO,会有虚拟的映射信息。下面的图是用户进程做的工作,这个工作可能调用系统方法库,但是具体执行可能跨内核进程和用户进程(如下图就是内核和用户进程协作流程图,channel处理可能分别对应用户子线程)。
对于映射关系内核上实现方式的改进,poll和epoll,poll就是维护映射关系链表,来扫面链表匹配对应的channel关系。epoll实现的是红黑树,而且epoll实现了event IO事件由内核线程发信号给LWP线程,信号量会被用户空间的调度器接收到,然后调度到对应的线程,该LWP线程有对象信号处理的方法实现。
阻塞和非阻塞与poll和epoll就没有关系,只是epoll在IO上做了优化;java1.4 NIO是基于poll, 1.5就是基于epoll;AIO异步IO,就是线程监听IO之后就去做其他的事情了,当IO回调事件回来的时候就由另外的线程来执行回调的IO方法读取数据,伪代码如下:
thread(){
new thread{
//子线程异步 do ohters
socket = lister(port);
socket.setCallback(*void(readData(socket)))
compute(a,b)
}.start();
readData(socket){
status = socket.readBuffer(data)
if(status == "finish"){
listData = listData + data
sendData(listData)
}else if(status == "unfinish"){
listData = listData + data
}
}
}
在这里说到异步和中断调用的问题,其实异步在我们编码里面经常出现,那就是线程并行处理,这是唯一的方式(looper或者其他线程回调都是并发的两个线程)。对于中断调用更是无处不在,我们写的任何交互,页面响应都是中断;在这里插入多讲一些,桌面UI系统,一般应用程序主程序(桌面应用,移动端app)都是UI线程,就像我们写的web或者数据处理的程序一样,主入口main函数就是主线程的起点。但是UI线程只是在用户空间的称呼,在内核层面都是一样的,ui主线程和子线程都是对应Kthread.
要是浏览器,那么其主线程UI主线程不仅仅依靠系统GUI库来绘制window和其他ui元素,还会加载浏览器本身的渲染引擎来加载html&css文件来解析页面的dom结构,然后根据dom结构来绘制窗口中的页面,当时调用绘制阶段使用到系统GUI,也会使用到渲染引擎的属性来保证元素绘制的规范性和一致性。
对应的硬件通过IO口告知我们中断的发生,然后中断处理线程(一般是调度器激活其他线程执行),会对应根据中断id调用不同的方法或者逻辑,所以中断调用入口为内核通知入口,一般为创建中断响应的内核句柄以及对应的实现方法。伪代码如下
int IOBufferHandler(int eventId){
if (event == 0){
}
if (event == 1){}
...
}
//声明函数指针,作为回调处理
int(*pHandler)(int) = IOBufferHandler;
//把函数指针塞入内核事件响应入口
kernelIOEvent.setEventCallback(int(*pHandler)(int))
对于中断的响应处理,一般我们把处理逻辑的入口告知对方(对方拿到的是口入地址指针),要么我们一直在上层程序不断的轮询读取事件状态,只有这两种方式,不会再有其他的方式了。对于我们通常所说的web-client IO模型BIO和NIO完全不是上面的概念,对于我们请求web端需要load数据,那么web肯定需要创建一个子线程,子线程去read数据返回给我们,不然如果主线程read数据返回给我们会导致主线程堵塞,而没有办法响应其他请求。伪代码:
webServer{
response(){
thread{
data = ioRead();
http.send(data);
}.start()
}
}
那这样的话,每一个请求加载数据,都会创建一个子线程去处理加载数据,这样的话线程就会爆炸,服务端也就崩掉。那我们使用线程池,通过线程池加载数据。
webServer{
response(){
pool = new threadPool()
loadData(pool);
}
loadData(threadPool pool){
task = new runnable{
data = ioRead();
http.send(data);
}
pool.addTask(task)
}
}
但是如果很多的请求,一样会导致线程池得队列很长,导致请求等待时间很长。因为每一个请求任务中读取IO的方法是阻塞的,这个请求连接的IO必须有数据返回并且读取结束了,任务执行才算是结束了。所以就算使用线程池,每一个任务执行都要按顺序把连接请求的数据读取结束。如果设计并发的IO读取机制,就是每一个IO操作都建立一个数据操作通道channel,系统监听到新的连接就会创建新的channel. 然后有一个识别机制Selector,它来判断是哪一个的通道数据读取回来了,从而选择哪一个channel去读取数据。那么我们只要塞给selector对应的回调函数指针即可,伪代码如下:
webServ{
listchannel = List;
main(){
listchannel = List<ioChannel>();
void(*pIOEvent)(int, byte[]) = dealIOEvent;
webSocketChannel.lister(listchannel, void(*pIOEvent)(int, byte[]));
//lister会一直监听端口,只要有连接,那么就会创建新的channel,
//并将其放到listchannel中
}
void dealIOEvent(int channelId, byte[] data){
for(chanl : listChannel){
if(chanl.id == channelId){
chanl.copy(data)
}
}
}
}
如果不是事件中断驱动,而是采用轮询方式实现呢,伪代码如下:
webServ{
listchannel = List;
main(){
listchannel = List<ioChannel>();
webSocketChannel.lister(listchannel);
//lister会一直监听端口,只要有连接,那么就会创建新的channel,
//并将其放到listchannel中
}
new thread{
dealIOEvent()
}.start();
void dealIOEvent(){
while(true){
for(chanl : listChannel){
if(chanl.status == "buffer_data"){
data = Selector.bufferData()//读取selector中数据
chanl.copy(data)
}
}
}
}
}
那上面说的selector+channel+buffer的机制就是Linux新的内核io非阻塞实现的方案(NIO)。早期(8.0版本之前)tomcat就是BIO模式(子线程)实现的,目前都是NIO实现方式。JBOSS(jboss衍生出netty 基于NIO)也是如此,还有apache Server (Apache衍生出NIO mina), nignx等。
五、优化实现网络连接和监听
对于上述的依据事件驱动的BIO,其实我们可以依据事件驱动做一定程度的优化,这就是业界中Reactor模式。做一个线程来监听socket.accept(), 然后事件驱动交给子线程处理任务。
webServ{
//单线程 reactor模式
main(){
websocket = new socket()
listCilent = List<Client>()
while(true){
client = websocket.accept();
if(client != null){
listCilent.add(client)
void(*pEventBuffer)(byte[]) = readCallBack;
client.setBuferEvent(void(*pEventBuffer)(byte[]))
}
}
void readCallBack(byte[] data){
readData(data)
}
}
}
紧接着做成多线程异步处理:
webServ{
//多线程 reactor模式
main(){
websocket = new socket()
listCilent = List<Client>()
while(true){
client = websocket.accept();
if(client != null){
listCilent.add(client)
void(*pEventBuffer)(byte[]) = readCallBack;
client.setBuferEvent(void(*pEventBuffer)(byte[]), client)
}
}
void readCallBack(byte[] data,client){
pool= new threadPool()
task = new runnable(data){
readData(data,client)
}
pool.addtask(task)
}
}
}
主从Reactor模式:
webServ{
//多线程 reactor主从模式
main(){
websocket = new socket()
listCilent = List<Client>()
childThread = Thread();
while(true){
client = websocket.accept();
if(client != null){
void(*dispacherTask(client)) = dispacher;
childThread.settaskList(void(*dispacherTask(client)))
childThread.start()
}
}
dispacher(client){
listCilent.add(client)
void(*pEventBuffer)(byte[]) = readCallBack;
client.setBuferEvent(void(*pEventBuffer)(byte[]), client)
}
void readCallBack(byte[] data,client){
pool= new threadPool()
task = new runnable(data){
readData(data,client)
}
pool.addtask(task)
}
}
}
目前ngnix,jboss,apache等web容器,等采用上述模式的不等方式支持高并发连接和响应数据请求。
六、回归select、poll与epoll
基于上面一堆的论述,防止阻塞就开子线程处理,为了实现异步就通过函数指针事件回调的机制处理;但是内核中的io事件能允许用户塞入函数指针,然后实现异步调用吗?这是一个极大风险问题的,相当于暴露内核给用户了,所以系统io不存在严格异步事件这么一说。所以上面扯得一堆函数指针实现的事件回调在io事件中是不存在的,只是我们认识了什么才是真正的异步处理机制。无论是poll还是epoll我们都需要while(1)死循环的不断轮询查阅io句柄的状态或者事件标志位来发现是否有数据返回。
select(select维护的句柄数是1024)之后都是多路复用io通道,是自己维护文件句柄列表和fdset的位图bitmap,并且反复产生用户态和内核态之间的拷贝,poll和select相似只是系统函数维护了连接句柄的链表。poll代码示例
int main(int argc,char *argv[]){
int lfd,cfd;
int maxi,i,nready,ret;
char buf[BUFSIZ],clie_ip[INET_ADDRSTRLEN];
struct pollfd client[OPEN_MAX];
struct sockaddr_in srv_addr,clt_addr;
socklen_t clt_addr_len;
bzero(&srv_addr, sizeof(srv_addr));
srv_addr.sin_family=AF_INET;
srv_addr.sin_port= htons(PORT);
srv_addr.sin_addr.s_addr= htonl(INADDR_ANY);
lfd= Socket(AF_INET,SOCK_STREAM,0);
int opt=1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt, sizeof(opt));
Bind(lfd,(struct sockaddr*)&srv_addr, sizeof(srv_addr));
Listen(lfd,128);
client[0].fd=lfd;
client[0].events=POLLIN;
//client[0].revents=0;
maxi=0; /* client[]数组有效元素中最大元素下标 */
for(i=1;i<OPEN_MAX;i++){
client[i].fd=-1;
}
while(1){
nready= poll(client,maxi+1,-1);
/*if(nready==-1){
if(errno == EINTR){
continue;;
} else{
sys_err("poll error");
}
}*/
if(client[0].revents & POLLIN) {
clt_addr_len = sizeof(clt_addr);
cfd = Accept(lfd, (struct sockaddr *) &clt_addr, &clt_addr_len);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &(clt_addr.sin_addr.s_addr), clie_ip, sizeof(clie_ip)),
ntohs(clt_addr.sin_port));
for (i = 1; i < OPEN_MAX; i++) {
if (client[i].fd == -1) {
client[i].fd = cfd;
client[i].events = POLLIN;
client[i].revents = 0;
break;
}
}
if (i == OPEN_MAX) {
sys_err("too many clients");
}
if (i > maxi) {
maxi = i;
}
if (--nready == 0) {
continue;
}
}
for (i = 1; i <= maxi; i++) {
if (client[i].fd == -1) {
continue;
}
if (client[i].revents & POLLIN) {
ret = read(client[i].fd, buf, sizeof(buf));
printf("----------%d\n", ret);
if (ret == 0) {
Close(client[i].fd);
client[i].fd == -1;
} else if (ret < 0) { //ECONNRESET
sys_err("read");
} else {
//write(STDOUT_FILENO,buf,ret);
for (int j = 0; j < ret; j++) {
buf[j] = toupper(buf[j]);
}
write(STDOUT_FILENO, buf, ret);
write(client[i].fd, buf, ret);
}
if (--nready <= 0) { //有事件,且处理了事件才减1!!!
break;
}
}
}
}
Close(lfd);
return 0;
}
可看poll需要传递所有的client列表,系统装载列表中的状态数据;poll返回列表中的事件总数,然后用户自己再去遍历client列表根据状态去读取数据。
epoll的改进在于改进系统维护的链表,改编成红黑树;然后epoll调用之后会返回遍历红黑树之后返回io事件的列表。
#include <stdio.h>
#include <ctype.h>
#include "wrap.h"
#include <sys/epoll.h>
int main()
{
int lfd;
int cfd;
int i;
int k;
char buf[1024];
int nready;
int n;
int sockfd;
//创建socket
lfd = Socket(AF_INET, SOCK_STREAM, 0);
//设置端口复用
int opt;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
//绑定
struct sockaddr_in serv;
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
serv.sin_addr.s_addr = htonl(INADDR_ANY);
Bind(lfd, (struct sockaddr *)&serv, sizeof(struct sockaddr_in));
//监听
Listen(lfd, 128);
//创建一棵epoll树
int epfd = epoll_create(1024);
if(epfd < 0)
{
perror("create epoll error");
return -1;
}
//将监听文件描述符lfd上树
struct epoll_event ev;
ev.data.fd = lfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event events[1024];
while(1)
{
nready = epoll_wait(epfd, events, 1024, -1);
if(nready < 0)
{
if(errno == EINTR)
{
continue;
}
break;
}
for(i = 0; i < nready; i++)
{
sockfd = events[i].data.fd;
//有客户端连接请求到来
if(sockfd == lfd)
{
cfd = Accept(lfd, NULL, NULL);
//将cfd文件描述符进行上树操作
ev.data.fd = cfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
continue;
}
//有客户端数据发送
memset(buf, 0x00, sizeof(buf));
n = Read(sockfd, buf, sizeof(buf));
if(n<=0)
{
close(sockfd);
//将sockfd对应的文件描述符从epoll树上删除
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
perror("read error or client closed");
continue;
}
else
{
printf("n==[%d], buf==[%s]", n, buf);
for(k = 0; k < n; k++)
{
buf[k] = toupper(buf[k]);
}
Write(sockfd, buf, n);
}
}
}
Close(epfd);
close(lfd);
return 0;
}
epoll还有一个很重要的改进就是设计了nmap映射区,copy数据到用户空间不需要用户态和内核态的来回切换。epoll的LT和ET模式:
1 epoll默认情况下是LT模式, 在这种模式下, 若读数据一次性没有读完,缓冲区中还有可读数据, 则epoll_wait还会再次通知
2 若将epoll设置为ET模式, 若读数据的时候一次性没有读完, 则epoll_wait不再通知,直到下次有新的数据发来.
七、高级语言的虚拟机构建LWP对应关系
- 用户空间运行线程库,任何应用程序都可以通过使用线程库被设计成多线程程序。线程库是用于用户级线程管理的一个例程包,它提供多线程应用程序的开发和运行的支撑环境,包含:用于创建和销毁线程的代码、在线程间传递数据和消息的代码、调度线程执行的代码以及保存和恢复线程上下文的代码。
- 所以线程的创建、消息传递、线程调度、保存/恢复上下文都由线程库来完成。内核感知不到多线程的存在。内核继续以进程为调度单位,并且给该进程指定一个执行状态(就绪、运行、阻塞等)
使用Java,开启一个线程,在这个线程里实现线程的切换(不太准确,其实Java开启一个线程,就是创建了一个用户线程,同时创建了一个内核线程),所以对于内核来说感知不到用户线程的存在,如果多个用户线程切换到某个线程在执行过程中IO阻塞,此时主线程(进程)就进入阻塞态,那么所有的用户现场都会阻塞。
协程就是用户态的线程(好像在java线程里面自己实现的线程),用户线程的特点:创建销毁快,支持大量的用户线程(创建用户线程比内核线程需要的空间要小的多),但是不能利用CPU多核,同时需要自己实现阻塞调度,否则会影响其他用户线程的执行。Go 语言是使用了 N:M 线程模型实现了自己的调度器,它在 N 个内核线程上多路复用(或调度)M 个协程,协程的上下文切换是在用户态由协程调度器完成的,因此不需要陷入内核,相比之下,这个代价就很小了。
- 程序是存储在内存中的指令,用户态线程LWP是可以准备好程序让内核态线程执行的。
- 用户线程把命令准备好,放入相应的指令空间中,内核线程会从里面取来进行执行
- 执行1+1只需要用户线程,在用户空间执行就行,把指令考入相应的内存中,内核线程获取时间片后取指执行.
对于高级语言比如java,golang,c#等,他们经过编译之后都会生成中间代码语言IL;然后再通过GLR(Common language runtime)或者JIT来转换成机器语言。所以中间代码转换机器代码是该语言编写的程序进程中一部分工作,对于转换成机器语言这部分也是有区别的,java/c#都是在转汇编机器码的时候,函数栈是依靠寄存器来返回数值的(这个c语言编译之后是一致的),而golang语言则依据栈本身。(下图是依靠寄存器返回函数的返回值:eax寄存器),而golang多参数传输,依靠pop出栈,传输函数返回值。
八、对操作系统与编译器的底层思考
我们都知道前些时间华为发布了鸿蒙操作系统和欧拉操作系统,鸿蒙操作系统是针对桌面和终端上的操作系统,针对x86架构和arm架构桌面的;而欧拉操作系统是针对服务器的操作系统。关键是其开发的操作系统的时候使用的是华为自己研发的方舟编译器,这款编译器在编译c代码的时候,做了很多链接库上还有指令优化上的工作,使得鸿蒙操作系统在运行的时候更加高效。
所以可见:同一种语言,不管你处于什么层次,哪怕开发的是操作系统,该语言的性能和编译器也密切相关。
那么华为开发操作系统,也有了编译器内核的自研,那么直接对外公开,各个企业就可以使用编译器内核开发鸿蒙操作系统或者欧拉操作系统的软件。有一点需要重点说明:首先第一步还得有基于鸿蒙操作开发一套基于自己编译器内核的IDEA,这个工作还是谁来做,一般都是系统发布的厂商,因为其他公司还不熟悉该系统的api系统,需要把该系统的api包(假如说是HomemyApi.lib)导入到或者嫁接到某一个其他系统(假如说是windows)上工具里面(假如说是virtual studio),然后通过vs开发运行在该系统上的工具(还需要将vs做变更,需要打包出来鸿蒙需要的安装包文件)。估计这个华为也已经开发了,就是基于鸿蒙操作系统的一堆的api方法库开发图形界面的软件开发工具,且此工具基于华为自身的编译器内核,然后有了此工具,其他公司才会陆续借助该工具开发其他语言的运行时工具甚至时编译器,如jvm,JavaScript执行引擎(甚至浏览器内核),以及基于这些运行时或者编译器的一套工具。接着使用这些工具开发办公软件或者各个文件阅读器等。所以一套新的操作系统的发布,也需要对应发布其基础上的可运行的编译器工具,但是这个编译器工具可以基于某语言开源的编译器内核,只是基于该编译器内核基础上开发运行在该操作系统上的可执行的编译器。