闲逛验证码江湖

验证码介绍

​ 图形验证码(CAPTCHA),是Completely Automated Public Turing Test to Tell Computers and Humans Apart (全自动区分计算机和人类的图灵测试)的简称。其本质是一种区分用户是计算机还是人的公共全自动程序。可以有效的防止某些特定程序以暴力方式不断进行登录尝试。验证码的不断发展其实是随着其破解功能的逐步强大而跟着演进的,这是一个攻防博弈的精彩世界。

​ 我们首先从手写几个简单的验证码开始,拉开验证码技术的面纱。

一、手写图形验证码

​ 首先用SpringBoot搭建一个简单的Web应用,有一个登录页面,也带好了简单的登录功能。目前这样的登录页面,对于同一个用户,每次登录都只是输入相同的用户名和密码,没有任何变数。我们就可以尝试给他添加一点变数,就是图形验证码功能。
在这里插入图片描述

首先在后台增加一个生成图形验证码的工具:

package com.roy.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Random;

/**
 * @author :楼兰
 * @date :Created in 2021/1/14
 * @description:
 **/

public class ImageUtil {
    private Logger logger = LoggerFactory.getLogger(ImageUtil.class);
    private ByteArrayInputStream image;//图像

    private String str;//验证码
    private int width = 352;
    private int height = 46;

    private ImageUtil() {
        init();//初始化属性
    }

    public static ImageUtil Instance() {
        return new ImageUtil();
    }

    /*
     * 取得验证码图片
     */
    public ByteArrayInputStream getImage() {
        return this.image;
    }

    /*
     * 取得图片的验证码
     */
    public String getString() {
        return this.str;
    }

    private void init() {
        this.str = "";
        //       在内存中创建图象
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        //       获取图形上下文
        Graphics g = image.getGraphics();
        //      生成随机类
        Random random = new Random();
        //       设定背景色
        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        //      设定字体
        g.setFont(new Font("Times New Roman", Font.PLAIN, 18));
        //       随机产生155条干扰线,使图象中的认证码不易被其它程序探测到
        //g.setColor(getRandColor(160, 200));
        //for (int i = 0; i < 155; i++) {
        //    int x = random.nextInt(width);
        //    int y = random.nextInt(height);
        //    int xl = random.nextInt(12);
        //    int yl = random.nextInt(12);
        //    g.drawLine(x, y, x + xl, y + yl);
        //}
        //       取随机产生的认证码(6位数字)
        String sRand = "";
        for (int i = 0; i < 6; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            // 将认证码显示到图象中
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            //      调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
            g.drawString(rand, (width/6) * i + 6, 46);
            this.str += rand;/*   赋值验证码   */
        }
        //       图象生效
        g.dispose();
        ByteArrayInputStream input = null;
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        try {
            ImageOutputStream imageOut = ImageIO.createImageOutputStream(output);
            ImageIO.write(image, "JPEG", imageOut);
            imageOut.close();
            input = new ByteArrayInputStream(output.toByteArray());
        } catch (Exception e) {
            System.out.println("验证码图片产生出现错误:" + e.toString());
        }

        this.image = input;/*  赋值图像  */
    }

    /*
     * 给定范围获得随机颜色
     */
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) fc = 255;
        if (bc > 255) bc = 255;
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }
}

然后在后台LoginController中添加一个生成验证码的方式,同时在登录端口增加一个简单的检查验证码的逻辑:

package com.roy.controller;

import com.roy.util.ImageUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @author :楼兰
 * @date :Created in 2021/1/14
 * @description:
 **/
@RestController
@RequestMapping("/common/")
public class LoginController {

    @PostMapping("/login")
    public Object login(String verifyCode,HttpServletRequest request){
        String sessionVerifyCode = request.getSession().getAttribute("verifyCode").toString();
        if(sessionVerifyCode.equals(verifyCode)){
            return "login";
        }else{
            return null;
        }
    }
    
    @GetMapping("/verifyCode")
    public void generateImg(HttpServletRequest request,HttpServletResponse response) throws IOException {
        response.setContentType("image/jpeg");
        ImageUtil imageUtil = ImageUtil.Instance();
        ByteArrayInputStream image = imageUtil.getImage();
        request.getSession().setAttribute("verifyCode",imageUtil.getString());
        byte[] bytes = new byte[1024];
        try(ServletOutputStream out = response.getOutputStream()){
            while(image.read(bytes)!=-1){
                out.write(bytes);
            }
        }
    }
}

然后在前端就可以增加一个img标签将验证码显示出来

<img src="/common/verifyCode" onclick="changeValidateCode(this)">
<script>
        function changeValidateCode(obj) {
            //获取当前的时间作为参数,无具体意义
            var timenow = new Date().getTime();
            //每次请求需要一个不同的参数,否则可能会返回同样的验证码
            //这和浏览器的缓存机制有关系,也可以把页面设置为不缓存,这样就不用这个参数了。
            obj.src="/common/verifyCode?d="+timenow;
        }
</script>

这样就完成了一个简单的图形验证码,并且点击图片可以刷新出新的验证码图片。

在这里插入图片描述

后续也可以在登录时增加对验证码的判断。需要输入正确的图形验证码才能登录成功。

现在我们来思考下这个验证码的作用。这个验证码对于正常用户,似乎只是让登录变得更麻烦了一点,而并没有起到什么实质性的作用。其实他最大的作用是让一些不友好的计算机程序无法正常登录。例如一些爬虫程序,可以在骗取用户的用户名和密码后,就可以用机器脚本模拟用户输入用户名和密码等这些页面操作,继而大规模爬取用户登录后的一些业务敏感信息。而验证码的作用就是让程序无法自动识别图形中的数字,从而阻止爬虫程序进行登录操作。

我们这个简单的图形验证码已经可以一定程度上保护登录操作了,爬虫程序在进行登录时,是不知道要输入什么样的验证码的。但是这样的验证码有什么问题呢?接下来做个小实验来尝试用机器的方式破解这些验证码。

验证码的挑战:

​ 这里我们尝试用一个开源的tess4j框架来对验证码图片进行OCR光学识别。

​ 首先,将生成的验证码图片保存到本地D:\verifyCode目录。我们多保存几张,用来测试破解的成功率。

在这里插入图片描述
先在pom文件中增加一个依赖

		<dependency>
            <groupId>net.sourceforge.tess4j</groupId>
            <artifactId>tess4j</artifactId>
            <version>4.5.4</version>
        </dependency>

然后需要到tess4j的官方git仓库上下载各种语言的训练数据。下载地址: https://github.com/tesseract-ocr/tessdata

其中,chi_sim.traineddata就是中文的训练数据,而eng.traineddata是英文的训练数据。我们把这两个文件下载下来放到D:\tessdata目录下。

然后就可以编写一个简单的程序来测试下之前保存的验证码的安全程度。

public class Tess4jDemo {
    public static void main(String[] args) throws TesseractException {
        ITesseract instance = new Tesseract();
        instance.setDatapath("D:\\tessdata");
        instance.setLanguage("chi_sim");

        File imageLocation = new File("D:\\verifyCode");
        for(File image: imageLocation.listFiles()){
            System.out.println(image.getName()+">>>"+instance.doOCR(image));
        }
    }
}

看一下实验的结果:

verifyCode.jpg>>>0 7 0 9 7 6
verifyCode1.jpg>>>9 7 7 4 3 Z
verifyCode10.jpg>>>0 4 3 1 5 5
verifyCode2.jpg>>>6 0 8 3 Z 6
verifyCode3.jpg>>>3 Z 0 5 Z Z
verifyCode4.jpg>>>3 Z 0 5 Z Z
verifyCode5.jpg>>>3 Z 0 5 Z Z
verifyCode6.jpg>>>5 0 6 9 0 0
verifyCode7.jpg>>>4 1 5 7 8 0
verifyCode8.jpg>>>9 8 9 1 1 4
verifyCode9.jpg>>>0 4 3 1 5 5

有没有看出问题来? 这次简单的实验中,除了数字2的识别不太准备,其他数字基本上全部识别正确。这意味着,我们这个简单的验证码基本防不住爬虫程序的破解,爬虫程序只需要稍微尝试几次就能绕过我们手写的这个验证码。

验证码的安全升级:

理解了这个问题之后,再回头看看我们之前的验证码生成工具,其中注释了的画155条随机干扰线的代码,就能够理解他作用了把。就是希望让验证码图片变得更复杂,在不影响真人之别的前提下,降低破解程序的成功率。我们在网上看到的各种奇形怪状的验证码,也正是出于同样的目的。

但是从刚才的简单实验中也能看到,tess4j这样的程序对于不同语言的识别准确率,都是通过训练集来提升的,这也意味着不管你的验证码变得多么复杂难看,只要有一定的规律,tess4j就可以用机器学习的方式,把他作为一种新的语言来识别,从而提高识别成功率。实际上,验证码识别也是机器学习与深度学习技术的一个非常热门的研究领域。这是个动态的攻防领域,所以验证码也需要有其他更多的技术来增加破解难度。

二、手写表达式验证码

为了提高验证码的安全性,将原始的图形验证码改成表达式验证码就是一个很常用的手段。其实本质也就是将展现出来的图形与保存到session中的验证码文字进行区分。例如图形展示一个随机的计算式,而将计算式的结果保存到session中来进行验证。

所以我们只需要对ImageUtil做一下修改。

package com.roy.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Random;

/**
 * @author :楼兰
 * @date :Created in 2021/1/14
 * @description:
 **/

public class ImageUtil {
    private Logger logger = LoggerFactory.getLogger(ImageUtil.class);
    private ByteArrayInputStream image;//图像

    private String str;//验证码
    private int width = 352;
    private int height = 46;

    private ImageUtil() {
        init();//初始化属性
    }

    public static ImageUtil Instance() {
        return new ImageUtil();
    }
    /*
     * 取得验证码图片
     */
    public ByteArrayInputStream getImage() {
        return this.image;
    }
    /*
     * 取得图片的验证码
     */
    public String getString() {
        return this.str;
    }
    private void init() {
        this.str = "";
        //       在内存中创建图象
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        //       获取图形上下文
        Graphics g = image.getGraphics();
        //      生成随机类
        Random random = new Random();
        //       设定背景色
        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        //      设定字体
        g.setFont(new Font("Times New Roman", Font.PLAIN, 18));
        //       随机产生155条干扰线,使图象中的认证码不易被其它程序探测到
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }
		//页面上画一个计算表达式
        int num1 = random.nextInt(10);
        int num2 = random.nextInt(10);
        g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
        g.drawString(String.valueOf(num1),(width/5)*0+6,36);
        g.drawString("+",(width/5)*1+6,36);
        g.drawString(String.valueOf(num2),(width/5)*2+6,36);
        g.drawString("=",(width/5)*3+6,36);
        g.drawString("?",(width/5)*4+6,36);
		//验证码保存计算结果
        this.str = ""+(num1+num2);
        //       图象生效
        g.dispose();
        ByteArrayInputStream input = null;
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        try {
            ImageOutputStream imageOut = ImageIO.createImageOutputStream(output);
            ImageIO.write(image, "JPEG", imageOut);
            imageOut.close();
            input = new ByteArrayInputStream(output.toByteArray());
        } catch (Exception e) {
            System.out.println("验证码图片产生出现错误:" + e.toString());
        }

        this.image = input;/*  赋值图像  */
    }

    /*
     * 给定范围获得随机颜色
     */
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) fc = 255;
        if (bc > 255) bc = 255;
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }
}

这样就可以实现一个简单的表达式验证码:

在这里插入图片描述
这样,我们在页面上画出来的是一个计算表达式,而实际存到Session中的只是一个计算的结果。在后续登录判断时,就可以要求用户输入正确结果才能通过验证。

这样我们手写实现了自己的表达式验证码,安全性得到了一定程度的提高。

三、体验开源验证码

​ 之前提到过,验证码技术是一个不断对抗升级的发展过程,接下来就介绍一些互联网上常用的开源验证码,带大家对验证码的技术环境有个大体的了解。以下这些组件也都在Demo工程中集成了进来。

1、jcaptcha http://jcaptcha.sourceforge.net/

这是一个java领域比较火的验证码框架,支持文字验证码、图片验证码以及语音验证码等多种形式。

2、Happy-Captcha https://gitee.com/ramostear/Happy-Captcha

一款实用非常简单的java验证码软件包。 支持图片和动画两种验证码风格,以及数字、中文、表达式等多种验证码类型。

类似的还有 https://gitee.com/whvse/EasyCaptcha

3、kcaptcha https://gitee.com/baomidou/kaptcha-spring-boot-starter

kcaptcha是Google开源的一款图形验证码框架,列出的这个是基于kcaptcha做出的springboot集成插件。

以上几款都是互联网上一些开源的图形验证码。这一类验证码其实跟我们之前手写的两个验证码属于同一类,都是通过在前端组织更复杂,更难以辨认的图形来加大破解难度。但是理论上,这一类图形验证码都可以通过我们试验过的OCR技术来进行破解,因为,本质上,我们人也是通过光学识别来判断图形中的内容的,只是人的行为比目前的技术要高级很多罢了。

下面介绍两款复杂点的行为验证码。这类验证码需要用户在网页上进行人机交互来完成验证。

4、滑块式验证码: https://gitee.com/LongbowEnterprise/SliderCaptcha

这个验证码可以实现滑块验证码,并且对用户的轨迹也有一些简单的判断逻辑。

5、AJ-Captcha: https://gitee.com/anji-plus/captcha?_from=gitee_search
这个验证码可以实现滑块验证码和点选汉字验证码。

这些行为验证码,很明显就无法通过OCR光学识别来破解了,还需要加入大量的人机交互操作。而这些人机交互操作,目前阶段也是可以通过类似于Selenium这样的组件来进行模拟的,例如点击鼠标、拖动滑块、松开鼠标,甚至输入文字等都可以模拟。因此,在安全性更高的场景下,还需要有更强大的验证码出现。

四、介绍商业验证码

以下介绍几个比较有名的商业验证码。这些商业验证码通常都是通过在前端定义更高级的人机交互操作,来达到更难被计算机程序模拟破解的目的。互联网的很多商业场景中,经常会采用一种或者多种验证码整合的方式来提高验证码安全性。

极验验证码 https://www.geetest.com/

​ 这是最为有名的一个专门做验证码的互联网产品,其中最有特色的是他这种点选式的验证码。只需要简单的点选中间的圆点就可以判断是人为操作还是机器模拟。这种验证码方式也是由极验最早推出,并开始逐渐推广到了其他商业验证码中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5FLmS7Tu-1610854783623)(img/5.png)]
在这里插入图片描述

​ 具体的实现机制无从获取,官方的说法是获取鼠标在页面上的移动轨迹,利用机器学习来判断操作对象是人还是计算机程序。不过以前曾经尝试过破解这个验证码,确实很难成功。而目前,越来越多的互联网公司都会引入这种验证码,再结合其他行为式验证码来形成组合策略。

网易网盾 https://dun.163.com/product/captcha

顶象科技 https://www.dingxiang-inc.com/business/captcha

腾讯天御验证码 https://cloud.tencent.com/product/captcha

五、验证码的其他实现方式

​ 以上讨论到的这些验证码,都是同步的人机交互验证码,通过定义更复杂的人机交互操作来达到提高破解难度的目的。而随着互联网逐渐走向多渠道联通,引入其他交互渠道来进行异步的人机交互,也成为一种非常好的验证码方式。例如目前常用的短信验证码、邮箱验证码、语音验证码等等的方式。

六、验证码的未来发展

​ 之前提到过,验证码技术其实是一个攻防博弈的动态发展的技术,因此,随着验证码发展得越来越安全,相应的破解技术也跟着不断发展,随之而来的,是双方的资源成本越来越高。

​ 例如对于互联网应用来说,商业验证码、短信验证码等这些验证码,如果应用的业务流程控制不好,很容易被羊毛党、黑客等人利用,造成成本极大浪费。而网上逐渐出现的各种奇葩验证码,人眼难辨,也让很多正常用户叫苦不迭。

​ 对于破解方来说,随着机器学习以及人工打码等技术的不断发展,可选择的技术手段也越来越多。验证码技术,也在破解方的大量资源投入下,变得越来越鸡肋。并且,很多互联网应用逐渐复杂的验证方式,安全性提高的同时,也提高了很多正常用户的使用门槛,成为了各种电信诈骗的温床。验证码技术逐渐开始偏离了最初的初衷。

​ 而未来的验证码技术,一方面,会通过引入更多的验证元素来进一步提高验证码的安全性,例如刷脸、刷指纹、语音交互、点选你购买过的商品 等。另一方面,通过对行为式验证码的研究逐渐深入,可以从传统的面向结果的验证,转化成面向过程的行为验证,并逐渐减少人工参与,降低关键信息被劫持的风险,形成更多对用户无感知的验证方式。例如分析用户鼠标轨迹、按键频率、使用习惯等。

​ 总之,验证码技术,是一个所有人都将亲身参与的精彩世界。

详细视频详解参见B站视频:https://www.bilibili.com/video/BV1eV411q76a
示例代码会上传到码云上。https://gitee.com/tearwind/CaptchaDemo

实现中断的模拟需要考虑到多个方面,包括中断的种类、中断的触发方式、中断的处理流程等。以下是一些可能的实现思路和代码示例: 1. 中断种类和触发方式 在模拟中断时,可以考虑三种不同的中断类型:程序结束中断、I/O 中断和时钟中断。其中,程序结束中断是软中断,需要在程序执行结束时显式触发;I/O 中断可以通过设置阻塞进程的等待时间来触发;时钟中断则可以通过设定时间片来触发。 在 CPU() 函数中,可以在每次执行指令之前都检查 PSW 寄存器,确认是否有中断需要处理。如果有,就进行相应的中断处理;否则,继续解释指令。 2. 中断处理流程 中断处理流程需要根据不同类型的中断来定制。例如,程序结束中断需要撤销当前进程并进行进程调度;I/O 中断需要唤醒阻塞的进程,让它们重新进入就绪队列;时钟中断则需要将当前进程转入就绪状态,并进行进程调度。 以下是一个示例代码,展示了如何在 Java 中实现基于相对时钟的进程调度和中断模拟。在这个示例中,使用了一个简单的进程控制块 PCB 类,用于描述进程的状态和属性。另外,定义了一个全局的进程列表 procList,用于存储所有的进程。 ```java // 进程控制块类 class PCB { public int pid; // 进程 ID public int pc; // 程序计数器 public int timeLeft; // 剩余时间片 public int state; // 进程状态(0:就绪,1:运行,2:阻塞,3:结束) public PCB(int pid, int pc, int timeLeft, int state) { this.pid = pid; this.pc = pc; this.timeLeft = timeLeft; this.state = state; } } // 进程列表 List<PCB> procList = new ArrayList<>(); // 初始化进程列表 procList.add(new PCB(0, 0, 5, 1)); // 进程0:运行状态,时间片为5 procList.add(new PCB(1, 0, 0, 0)); // 进程1:就绪状态,无时间片 procList.add(new PCB(2, 0, 0, 0)); // 进程2:就绪状态,无时间片 procList.add(new PCB(3, 0, 0, 0)); // 进程3:就绪状态,无时间片 // 中断处理函数 public static void handleInterrupt(int type) { switch (type) { case 0: // 程序结束中断 PCB curProc = procList.get(0); curProc.state = 3; // 结束状态 procList.remove(0); // 从进程列表中移除 schedule(); // 进程调度 break; case 1: // I/O 中断 // 唤醒阻塞的进程 for (PCB pcb : procList) { if (pcb.state == 2 && pcb.timeLeft == 0) { pcb.state = 0; // 就绪状态 } } break; case 2: // 时钟中断 PCB curProc = procList.get(0); curProc.timeLeft--; // 时间片减一 if (curProc.timeLeft == 0) { // 时间片用完,进程转为就绪状态 curProc.state = 0; schedule(); // 进程调度 } break; default: break; } } // 进程调度函数 public static void schedule() { // 判断当前进程是否为 idle 进程 if (procList.get(0).pid == -1) { // 如果是,直接选择就绪队列中的第一个进程 procList.remove(0); procList.get(0).state = 1; // 运行状态 procList.get(0).timeLeft = 5; // 时间片重置 return; } // 选择一个就绪进程 PCB nextProc = null; for (PCB pcb : procList) { if (pcb.state == 0) { nextProc = pcb; break; } } if (nextProc == null) { // 就绪队列为空,选择 idle 进程 nextProc = procList.get(0); nextProc.timeLeft = 5; // 时间片重置 } // 将当前运行进程转为就绪状态 procList.get(0).state = 0; // 将就绪进程移动到队首,并设置为运行状态 procList.remove(nextProc); procList.add(0, nextProc); nextProc.state = 1; } // CPU 函数 public static void CPU() { PCB curProc = procList.get(0); int opcode = getOpcode(curProc.pc); switch (opcode) { case 0: // end 指令 handleInterrupt(0); // 触发程序结束中断 break; case 1: // I/O 指令 curProc.timeLeft = 3; // 设置 I/O 时间 curProc.state = 2; // 阻塞状态 handleInterrupt(1); // 触发 I/O 中断 schedule(); // 进程调度 break; default: // 其他指令 curProc.pc++; // 程序计数器加一 curProc.timeLeft--; // 时间片减一 if (curProc.timeLeft == 0) { // 时间片用完,进程转为就绪状态 curProc.state = 0; schedule(); // 进程调度 } break; } } // 主函数 public static void main(String[] args) { // 初始化 idle 进程 procList.add(new PCB(-1, 0, 5, 1)); // 模拟 CPU 循环 while (true) { CPU(); } } ``` 在这个示例中,使用了一个相对时钟来实现时间片的中断。在每次 CPU() 函数执行完一条指令后,都会检查当前进程的时间片是否用完。如果用完了,就将当前进程转为就绪状态,并进行进程调度。同时,如果有其他类型的中断需要处理,也会在此时进行处理。另外,进程调度函数 schedule() 会根据当前就绪进程的情况,选择一个进程进行运行。如果就绪进程为空,就会选择一个 idle 进程来运行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

roykingw

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值