linux多线程_Java+Linux,深入内核源码讲解多线程之进程

之前写了两篇文章,都是针对Linux这个系统的,为什么?我为什么这么喜欢写这个系统的知识,可能就是为了今天的内容多线程系列,现在多线程不是一个面试重点 啊,那如果你能深入系统内核回答这个知识点,面试官会怎么想?你会不会占据面试的主动权(我不会说今天被一个面试者惊艳到了的)今天,我就开始一个系列的内容,多线程--高并发,深入的给大家讲解,我就不信讲不明白这么个小东西,有问题的地方希望大家能够指出,谢谢,大家一起成长

今天我们将第一个知识点:进程

Linux 内核如何描述一个进程?

1. Linux 的进程

进程的术语是 process,是 Linux 最基础的抽象,另一个基础抽象是文件。

最简单的理解,进程就是执行中 (executing, 不等于running) 的程序。

更准确一点的理解,进程包括执行中的程序以及相关的资源 (包括cpu状态、打开的文件、挂起的信号、tty、内存地址空间等)。

一种简洁的说法:进程 = n*执行流 + 资源,n>=1。

Linux 进程的特点:

  1. 通过系统调用 fork() 创建进程,fork() 会复制现有进程来创建一个全新的进程。
  2. 内核里,并不严格区分进程和线程。
  3. 从内核的角度看,调度单位是线程 (即执行流)。可以把线程看做是进程里的一条执行流,1个进程里可以有1个或者多个线程。
  4. 内核里,常把进程称为 task 或者 thread,这样描述更准确,因为许多进程就只有1条执行流。
  5. 内核通过轻量级进程 (lightweight process) 来支持多线程。1个轻量级进程就对应1个线程,轻量级进程之间可以共享打开的文件、地址空间等资源。

2. Linux 的进程描述符

2.1 task_struct

内核里,通过 task_struct 结构体来描述一个进程,称为进程描述符 (process descriptor),它保存着支撑一个进程正常运行的所有信息。

每一个进程,即便是轻量级进程(即线程),都有1个 task_struct。

 (include\linux)  struct task_struct {  struct thread_info thread_info;  volatile long state;  void *stack;  [...]  struct mm_struct *mm;  [...]  pid_t pid;  [...]  struct task_struct *parent;  [...]  char comm[TASK_COMM_LEN];  [...]  struct files_struct *files;  [...]  struct signal_struct *signal;  } 

这是一个庞大的结构体,不仅有许多进程相关的基础字段,还有许多指向其他数据结构的指针。

它包含的字段能完整地描述一个正在执行的程序,包括 cpu 状态、打开的文件、地址空间、挂起的信号、进程状态等。

b8156aaf4af221c9a483526105cba1dc.png

作为初学者,先简单地了解部分字段就好::

  • struct thread_info thread_info: 进程底层信息,平台相关,下面会详细描述。
  • long state: 进程当前的状态,下面是几个比较重要的进程状态以及它们之间的转换流程。
95884fe1dedd9d13e22c316decea5735.png
  • void *stack: 指向进程内核栈,下面会解释。
  • struct mm_struct *mm: 与进程地址空间相关的信息都保存在一个叫内存描述符 (memory descriptor) 的结构体 (mm_struct) 中。
aac69aabc7b0ed49420f75d1607af28a.png

pid_t pid: 进程标识符,本质就是一个数字,是用户空间引用进程的唯一标识。

struct task_struct *parent: 父进程的 task_struct。  char comm[TASK_COMM_LEN]: 进程的名称。  struct files_struct *files: 打开的文件表。  struct signal_struct *signal: 信号处理相关。 

其他字段,等到有需要的时候再回过头来学习。

当发生系统调用或者进程切换时,内核如何找到 task_struct ?

对于 ARM 架构,答案是:通过内核栈 (kernel mode stack)。

为什么要有内核栈?

因为内核是可重入的,在内核中会有多条与不同进程相关联的执行路径。因此不同的进程处于内核态时,都需要有自己私有的进程内核栈 (process kernel stack)。

当进程从用户态切换到内核态时,所使用的栈会从用户栈切换到内核栈。

至于是如何切换的,关键词是系统调用,这不是本文关注的重点,先放一边,学习内核要懂得恰当的时候忽略细节。

当发生进程切换时,也会切换到目标进程的内核栈。

同上,关键词是硬件上下文切换 (hardware context switch),忽略具体实现。

无论何时,只要进程处于内核态,就会有内核栈可以使用,否则系统就离崩溃不远了。

ARM 架构的内核栈和 task_struct 的关系如下:

d40d3e3a2a3d9abe57d1391026ec1986.png

内核栈的长度是 THREAD_SIZE,对于 ARM 架构,一般是 2 个页框的大小,即 8KB。

内核将一个较小的数据结构 thread_info 放在内核栈的底部,它负责将内核栈和 task_struct 串联起来。thread_info 是平台相关的,在 ARM 架构中的定义如下:

//  (arch\arm\include\asm)  struct thread_info {  unsigned long flags; /* low level flags */  int preempt_count; /* 0 => preemptable, <0 => bug */  mm_segment_t addr_limit; /* address limit */  struct task_struct *task; /* main task structure */  [...]  struct cpu_context_save cpu_context; /* cpu context */  [...]  }; 

thread_info 保存了一个进程能被调度执行的最底层信息(low level task data),例如struct cpu_context_save cpu_context 会在进程切换时用来保存/恢复寄存器上下文。

内核通过内核栈的栈指针可以快速地拿到 thread_info:

//  (include\linux)  static inline struct thread_info *current_thread_info(void)  {  // current_stack_pointer 是当前进程内核栈的栈指针  return (struct thread_info *)  (current_stack_pointer & ~(THREAD_SIZE - 1));  }  然后通过 thread_info 找到 task_struct:  // current.h (include\asm-generic)  #define current (current_thread_info()->task) 

内核里通过 current 宏可以获得当前进程的 task_struct。

2.3 task_struct 的分配和初始化

当上层应用使用 fork() 创建进程时,内核会新建一个 task_struct。

进程的创建是个复杂的工作,可以延伸出无数的细节。这里我们只是简单地了解一下 task_struct 的分配和部分初始化的流程。

fork() 在内核里的核心流程:

d7f61a06699bc91d52f8da637210d664.png

dup_task_struct() 做了什么?

288c018a989e82a06a7a7b76cfa15e28.png

至于设置内核栈里做了什么,涉及到了进程的创建与切换,不在本文的关注范围内,以后再研究了。

3. 实验:打印 task_struct / thread_info / kernel mode stack

实验目的:

  • 梳理 task_struct / thread_info / kernel mode stack 的关系。

实验代码:

实验代码: #include <linux/ #include <linux/ #include <linux/>  static void print_task_info(struct task_struct *task) {     printk(KERN_NOTICE "%10s %5d task_struct (%p) / stack(%p~%p) / thread_info->task (%p)",         task->comm,         task->pid,         task,         task->stack,         ((unsigned long *)task->stack) + THREAD_SIZE,         task_thread_info(task)->task); }  static int __init task_init(void) {     struct task_struct *task = current;      printk(KERN_INFO "task module init\n");      print_task_info(task);     do {         task = task->parent;         print_task_info(task);     } while (task->pid != 0);      return 0; } module_init(task_init);  static void __exit task_exit(void) {     printk(KERN_INFO "task module exit\n "); } module_exit(task_exit); 

运行效果:

task module init     insmod  3123 task_struct (edb42580) / stack(ed46c000~ed474000) / thread_info->task (edb42580)       bash  2393 task_struct (eda13e80) / stack(c9dda000~c9de2000) / thread_info->task (eda13e80)       sshd  2255 task_struct (ee5c9f40) / stack(c9d2e000~c9d36000) / thread_info->task (ee5c9f40)       sshd   543 task_struct (ef15f080) / stack(ee554000~ee55c000) / thread_info->task (ef15f080)    systemd     1 task_struct (ef058000) / stack(ef04c000~ef054000) / thread_info->task (ef058000) 

在程序里,我们通过 task_struct 找到 stack,然后通过 stack 找到 thread_info,最后又通过 thread_info->task 找到 task_struct。

到这里,不知道你对进程的概念是不是有了一个清晰的理解

但是上面是通过Linux进行了线程的展示,在日常的工作中,代码的实现和编写我们还是以Java为主,那我们来看一下Java进程

进程的创建

Java提供了两种方法用来启动进程或其它程序:

  • 使用Runtime的exec()方法
  • 使用ProcessBuilder的start()方法

ProcessBuilder

ProcessBuilder类是J2SE 1.5在中新添加的一个新类,此类用于创建操作系统进程,它提供一种启动和管理进程(也就是应用程序)的方法。在J2SE 1.5之前,都是由Process类处来实现进程的控制管理。

每个 ProcessBuilder 实例管理一个进程属性集。start() 方法利用这些属性创建一个新的 Process 实例。start() 方法可以从同一实例重复调用,以利用相同的或相关的属性创建新的子进程。

每个进程生成器管理这些进程属性:

  • 命令 是一个字符串列表,它表示要调用的外部程序文件及其参数(如果有)。在此,表示有效的操作系统命令的字符串列表是依赖于系统的。例如,每一个总体变量,通常都要成为此列表中的元素,但有一些操作系统,希望程序能自己标记命令行字符串——在这种系统中,Java 实现可能需要命令确切地包含这两个元素。
  • 环境 是从变量 到值 的依赖于系统的映射。初始值是当前进程环境的一个副本(请参阅 ())。
  • 工作目录。默认值是当前进程的当前工作目录,通常根据系统属性 来命名。
  • redirectErrorStream 属性。最初,此属性为 false,意思是子进程的标准输出和错误输出被发送给两个独立的流,这些流可以通过 () 和 () 方法来访问。如果将值设置为 true,标准错误将与标准输出合并。这使得关联错误消息和相应的输出变得更容易。在此情况下,合并的数据可从 () 返回的流读取,而从 () 返回的流读取将直接到达文件尾。

修改进程构建器的属性将影响后续由该对象的 start() 方法启动的进程,但从不会影响以前启动的进程或 Java 自身的进程。大多数错误检查由 start() 方法执行。可以修改对象的状态,但这样 start() 将会失败。例如,将命令属性设置为一个空列表将不会抛出异常,除非包含了 start()。

注意,此类不是同步的。如果多个线程同时访问一个 ProcessBuilder,而其中至少一个线程从结构上修改了其中一个属性,它必须 保持外部同步。

构造方法摘要

ProcessBuilder(List command)  利用指定的操作系统程序和参数构造一个进程生成器。  ProcessBuilder(String... command)  利用指定的操作系统程序和参数构造一个进程生成器。 

方法摘要

List command()  返回此进程生成器的操作系统程序和参数。  ProcessBuilder command(List command)  设置此进程生成器的操作系统程序和参数。  ProcessBuilder command(String... command)  设置此进程生成器的操作系统程序和参数。  File directory()  返回此进程生成器的工作目录。  ProcessBuilder directory(File directory)  设置此进程生成器的工作目录。  Map environment()  返回此进程生成器环境的字符串映射视图。  boolean redirectErrorStream()  通知进程生成器是否合并标准错误和标准输出。  ProcessBuilder redirectErrorStream(boolean redirectErrorStream)  设置此进程生成器的 redirectErrorStream 属性。  Process start()  使用此进程生成器的属性启动一个新进程。 

1.2 Runtime

每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。可以通过 getRuntime 方法获取当前运行时。

应用程序不能创建自己的 Runtime 类实例。但可以通过 getRuntime 方法获取当前Runtime运行时对象的引用。一旦得到了一个当前的Runtime对象的引用,就可以调用Runtime对象的方法去控制Java虚拟机的状态和行为。

Java代码 收藏代码

void addShutdownHook(Thread hook)  注册新的虚拟机来关闭挂钩。  int availableProcessors()  向 Java 虚拟机返回可用处理器的数目。  Process exec(String command)  在单独的进程中执行指定的字符串命令。  Process exec(String[] cmdarray)  在单独的进程中执行指定命令和变量。  Process exec(String[] cmdarray, String[] envp)  在指定环境的独立进程中执行指定命令和变量。  Process exec(String[] cmdarray, String[] envp, File dir)  在指定环境和工作目录的独立进程中执行指定的命令和变量。  Process exec(String command, String[] envp)  在指定环境的单独进程中执行指定的字符串命令。  Process exec(String command, String[] envp, File dir)  在有指定环境和工作目录的独立进程中执行指定的字符串命令。  void exit(int status)  通过启动虚拟机的关闭序列,终止当前正在运行的 Java 虚拟机。  long freeMemory()  返回 Java 虚拟机中的空闲内存量。  void gc()  运行垃圾回收器。  InputStream getLocalizedInputStream(InputStream in)  已过时。 从 JDK  开始,将本地编码字节流转换为 Unicode 字符流的首选方法是使用 InputStreamReader 和 BufferedReader 类。  OutputStream getLocalizedOutputStream(OutputStream out)  已过时。 从 JDK  开始,将 Unicode 字符流转换为本地编码字节流的首选方法是使用 OutputStreamWriter、BufferedWriter 和 PrintWriter 类。  static Runtime getRuntime()  返回与当前 Java 应用程序相关的运行时对象。  void halt(int status)  强行终止目前正在运行的 Java 虚拟机。  void load(String filename)  加载作为动态库的指定文件名。  void loadLibrary(String libname)  加载具有指定库名的动态库。  long maxMemory()  返回 Java 虚拟机试图使用的最大内存量。  boolean removeShutdownHook(Thread hook)  取消注册某个先前已注册的虚拟机关闭挂钩。  void runFinalization()  运行挂起 finalization 的所有对象的终止方法。  static void runFinalizersOnExit(boolean value)  已过时。 此方法本身具有不安全性。它可能对正在使用的对象调用终结方法,而其他线程正在操作这些对象,从而导致不正确的行为或死锁。  long totalMemory()  返回 Java 虚拟机中的内存总量。  void traceInstructions(boolean on)  启用/禁用指令跟踪。  void traceMethodCalls(boolean on)  启用/禁用方法调用跟踪。 

1.3 Process

不管通过哪种方法启动进程后,都会返回一个Process类的实例代表启动的进程,该实例可用来控制进程并获得相关信息。Process 类提供了执行从进程输入、执行输出到进程、等待进程完成、检查进程的退出状态以及销毁(杀掉)进程的方法:

void destroy()  杀掉子进程。  一般情况下,该方法并不能杀掉已经启动的进程,不用为好。  int exitValue()  返回子进程的出口值。  只有启动的进程执行完成、或者由于异常退出后,exitValue()方法才会有正常的返回值,否则抛出异常。  InputStream getErrorStream()  获取子进程的错误流。  如果错误输出被重定向,则不能从该流中读取错误输出。  InputStream getInputStream()  获取子进程的输入流。  可以从该流中读取进程的标准输出。  OutputStream getOutputStream()  获取子进程的输出流。  写入到该流中的数据作为进程的标准输入。  int waitFor()  导致当前线程等待,如有必要,一直要等到由该 Process 对象表示的进程已经终止。 

2.多进程编程实例

一般我们在java中运行其它类中的方法时,无论是静态调用,还是动态调用,都是在当前的进程中执行的,也就是说,只有一个java虚拟机实例在运行。而有的时候,我们需要通过java代码启动多个java子进程。这样做虽然占用了一些系统资源,但会使程序更加稳定,因为新启动的程序是在不同的虚拟机进程中运行的,如果有一个进程发生异常,并不影响其它的子进程。

在Java中我们可以使用两种方法来实现这种要求。最简单的方法就是通过Runtime中的exec方法执行java classname。如果执行成功,这个方法返回一个Process对象,如果执行失败,将抛出一个IOException错误。下面让我们来看一个简单的例子。

// 文件 import .*; public class Test {  public static void main(String[] args)  { FileOutputStream fOut = new FileOutputStream("c:\\"); fOut.close(); System.out.println("被调用成功!");  } }   //  public class Test_Exec {  public static void main(String[] args)  { Runtime run = (); Process p = run.exec("java test1");  } } 

通过java Test_Exec运行程序后,发现在C盘多了个文件,但在控制台中并未出现"被调用成功!"的输出信息。因此可以断定,Test已经被执行成功,但因为某种原因,Test的输出信息未在Test_Exec的控制台中输出。这个原因也很简单,因为使用exec建立的是Test_Exec的子进程,这个子进程并没有自己的控制台,因此,它并不会输出任何信息。

如果要输出子进程的输出信息,可以通过Process中的getInputStream得到子进程的输出流(在子进程中输出,在父进程中就是输入),然后将子进程中的输出流从父进程的控制台输出。具体的实现代码如下如示:

//  import .*; public class Test_Exec_Out {  public static void main(String[] args)  { Runtime run = (); Process p = run.exec("java test1"); BufferedInputStream in = new BufferedInputStream(()); BufferedReader br = new BufferedReader(new InputStreamReader(in)); String s; while ((s = ()) != null)  System.out.println(s);  } } 

从上面的代码可以看出,在中通过按行读取子进程的输出信息,然后在Test_Exec_Out中按每行进行输出。 上面讨论的是如何得到子进程的输出信息。那么,除了输出信息,还有输入信息。既然子进程没有自己的控制台,那么输入信息也得由父进程提供。我们可以通过Process的getOutputStream方法来为子进程提供输入信息(即由父进程向子进程输入信息,而不是由控制台输入信息)。我们可以看看如下的代码:

// 文件 import .*; public class Test {  public static void main(String[] args)  { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); System.out.println("由父进程输入的信息:" + ());  } }   //  import .*; public class Test_Exec_In {  public static void main(String[] args)  { Runtime run = (); Process p = run.exec("java test2"); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(())); ("向子进程输出信息"); (); bw.close(); // 必须得关闭流,否则无法向子进程中输入信息 // System.in.read();  } } 

从以上代码可以看出,Test1得到由Test_Exec_In发过来的信息,并将其输出。当你不加()和()时,信息将无法到达子进程,也就是说子进程进入阻塞状态,但由于父进程已经退出了,因此,子进程也跟着退出了。如果要证明这一点,可以在最后加上.read(),然后通过任务管理器(在windows下)查看java进程,你会发现如果加上()和(),只有一个java进程存在,如果去掉它们,就有两个java进程存在。这是因为,如果将信息传给Test2,在得到信息后,Test2就退出了。在这里有一点需要说明一下,exec的执行是异步的,并不会因为执行的某个程序阻塞而停止执行下面的代码。因此,可以在运行test2后,仍可以执行下面的代码。

exec方法经过了多次的重载。上面使用的只是它的一种重载。它还可以将命令和参数分开,如exec("")可以写成exec("java", "test2")。exec还可以通过指定的环境变量运行不同配置的java虚拟机。

除了使用Runtime的exec方法建立子进程外,还可以通过ProcessBuilder建立子进程。ProcessBuilder的使用方法如下:

//  import .*; public class Test_Exec_Out {  public static void main(String[] args)  { ProcessBuilder pb = new ProcessBuilder("java""test1"); Process p = (); … …  } } 

在建立子进程上,ProcessBuilder和Runtime类似,不同的ProcessBuilder使用start()方法启动子进程,而Runtime使用exec方法启动子进程。得到Process后,它们的操作就完全一样的。

ProcessBuilder和Runtime一样,也可设置可执行文件的环境信息、工作目录等。下面的例子描述了如何使用ProcessBuilder设置这些信息。

ProcessBuilder pb = new ProcessBuilder("Command""arg2""arg2"'''); // 设置环境变量 Map<String, String> env = (); ("key1""value1"); ("key2"); ("key2", ("key1") + "_test"); ("..\abcd"); // 设置工作目录 Process p = (); // 建立子进程 

【编辑推荐】

【责任编辑:

未丽燕

TEL:(010)68476606】


点赞 0

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/374503.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ipad连接电脑_这些应用让iPad生产力分分钟UP

IT时报见习记者 钱奕昀用iPad办公这件事&#xff0c;多年前网友就在讨论&#xff0c;最常见的还是那句“买前生产力&#xff0c;买后爱奇艺”。很长一段时间里&#xff0c;它的生产力属性都是弱于娱乐属性的。其实&#xff0c;作为PC端和移动端的形态中和&#xff0c;iPad可以…

Mac OSX 快捷键命令行

ctrlshift 快速放大dock的图标会暂时放大&#xff0c;而如果你开启了dock放大CommandOptionW 将所有窗口关闭CommandW 将当前窗口关闭(可以关闭Safari标签栏,很实用) CommandOptionM …

将JavaFX 2.0与Swing和SWT集成

JavaFX 2.0对JavaFX的改进之一是可以更轻松地与Swing和SWT进行互操作 。 一些在线资源记录了如何完成此操作。 其中包括将JavaFX集成到Swing应用程序和SWT Interop中 。 但是&#xff0c;在有效的类级Javadoc文档的一个很好的示例中&#xff0c;各自的JavaFX类javafx.embed.swi…

wifi rssi 计算 距离_WiFi和WLAN是一样的?真相在这里~别再傻傻分不清了

我们通常上网的时候会说连接WiFi如果注意到无线网络的名称就会发现手机的连接显示是WLAN别再将WiFI和WLAN搞混了&#xff01;二者的定义WLANWLAN的全称为 Wireless Local Area Networks,中文意思为无线局域网络&#xff0c;是一种数据传输系统。它是利用射频技术进行数据传输&a…

实用的it知识学习_怎样能更快更好的学习好书法?分享一些比较实用的理论知识...

如何能更快更高效的学习书法&#xff1f;首先了解一些书法理论知识是很有必要的&#xff01;它能让你在学习书法的过程中不至于迷茫 &#xff01;能助你更快学好书法&#xff01;一、书论在实践中产生我们大部分人都觉得学习书法可以没有理论&#xff0c;但不可无技法。但理论和…

android 上下滚动文字_计算机毕设项目004之Android系统在线小说阅读器

计算机毕设项目004之Android系统在线小说阅读器一. 项目名称基于Android系统的在线小说阅读器二. 项目简介项目中的角色功能&#xff1a;支持翻页动画:仿真翻页、覆盖翻页、上下滚动翻页等翻页效果。支持页面定制:亮度调节、背景调节、字体大小调节支持全屏模式(含有虚拟按键的…

基线检查工具_最新版CAD燕秀工具箱2.87(支持20042021)

好课推荐&#xff1a;零基础CAD&#xff1a;点我CAD家装&#xff1a;点我 周站长CAD&#xff1a;点我CAD机械&#xff1a;点我revit教程&#xff1a;点我CAD建筑&#xff1a;点我CAD三维&#xff1a;点我全屋定制&#xff1a;点我 ps教程&#xff1a;点我苹果版CAD:点我 3dmax教…

关于java.lang.ArithmeticException

java.lang.ArithmeticException “数学运算异常”&#xff0c;可能是自己的数学运算公式出现了错误、违反了数学运算规则。错误记录&#xff1a; 出错原因&#xff1a; a % b 中b不能为0

Java EE 6与Spring Framework:技术决策过程

在过去的几个月中&#xff0c;我们经历了这个决策过程&#xff1a;为Java平台上的企业开发选择哪种技术堆栈&#xff1f; 有多种选择。 但是&#xff0c;我们深入讨论的是&#xff1a;纯Java EE 6堆栈与带有Java EE的Spring。 以下博客文章总结了当您考虑这些技术堆栈选项之一时…

允许服务与桌面交互_vivo 正式推出 Origin OS,融合自然设计与全新交互

点击右上角关注我们&#xff0c;每天给您带来最新最潮的科技资讯&#xff0c;让您足不出户也知道科技圈大事&#xff01;今天下午&#xff0c;vivo 推出了全新 Origin OS 手机系统。它采用了源于自然界的设计理念&#xff0c;同时加入了全新并且允许用户进行深度自定义的交互方…

dll 源码_【技术分享】 | 一个JAVA内存马的源码分析

前言偶然接触到了这样一个JAVA内存马&#xff0c;其作者也是冰蝎的作者&#xff0c;项目地址&#xff1a;https://github.com/rebeyond/memShell正好最近在接触JAVA&#xff0c;借此机会学习下大佬的代码&#xff0c;对自己的编程思路也有了一定的提升。当然笔者只是一个脚本小…

ThunderSearch(闪电搜索器)_网络空间搜索引擎工具_信息收集

文章目录 ThunderSearch简介1 项目地址2 使用方式2.1 配置文件config.json说明2.2 构建和运行 3 使用式例 ThunderSearch简介 ThunderSearch&#xff08;闪电搜索器&#xff09;是一款使用多个(【支持Fofa、Shodan、Hunter、Zoomeye、360Quake网络空间搜索引擎】网络空间搜索引…

每个人都知道MVC…

从一个最近的博客中&#xff0c;您可能已经了解到我最近一直在进行一些采访&#xff0c;因为他们是针对Web应用程序开发人员的&#xff0c;所以我问的一个问题是“您能解释一下MVC模式是什么吗&#xff1f;”&#xff0c;值得称赞的是&#xff0c;每个候选人知道答案。 对于不认…

r语言ggplot2 多线图绘制图例_plotnine: Python版的ggplot2作图库

腾讯课堂 | Python网络爬虫与文本数据分析同样的基本作图任务&#xff0c;plotnine比matplotlib和seaborn代码量少&#xff0c;更美观。所以我又重新发一遍&#xff0c;大家可以先收藏起来&#xff0c;后面总有用到的时候~R语言的ggplot2绘图能力超强&#xff0c;python虽有mat…

单元和集成测试的代码覆盖率

我最近在一个宠物项目中着手构建自动化的UI&#xff08;集成&#xff09;测试以及普通的单元测试。 我想将所有这些集成到我的Maven构建中&#xff0c;并提供代码覆盖率报告&#xff0c;以便我可以了解测试覆盖率不足的区域。 我不仅发布了项目的源代码&#xff0c;还整理了一个…

python学生分布_Python数据分析实战之分布分析

前言 本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理。 作者&#xff1a;严小样儿 分布分析法&#xff0c;一般是根据分析目的&#xff0c;将数据进行分组&#xff0c;研究各组别分布规律的一种分析方法。…

hls fifo_HLS优化方法DATAFLOW你用了吗

上期内容&#xff1a;异步跨时钟域电路该怎么约束DATAFLOW作为HLS的一种优化方法&#xff0c;对于改善吞吐率(Throughput)、降低延迟(Latency)非常有效。DATAFLOW的作用对象DATAFLOW可以作用于函数&#xff0c;也可以作用于for循环。如下图所示(图片来源Figure62, Figure 63, u…

在Hibernate,EhCache,Quartz,DBCP和Spring中启用JMX

继续使用JMX的过程&#xff08;请参阅&#xff1a; 人类JMX &#xff09;&#xff0c;我们将学习如何在一些流行的框架中启用JMX支持&#xff08;通常是统计和监视功能&#xff09;。 这些信息大部分都可以在项目的主页上找到&#xff0c;但是我决定在收集这些信息的同时&#…

400多万微信用户如何“变现”?凯叔说了五大秘诀与教训

凯叔&#xff0c;原名王凯&#xff0c;自媒体“凯叔讲故事”创始人&#xff0c;近日在狮享家班委会上做了分享&#xff0c;全是实实在在的实验性方法论。以下是王凯的分享内容&#xff0c;整理 / 垅青 我讲的主题叫“基于内容的MVP探索”&#xff0c;MVP是什么东西&#xff1f;…

使用模拟的单元测试–测试技术5

我的最后一个博客是有关测试代码方法的一系列博客中的第四篇&#xff0c;演示了如何创建使用存根对象隔离测试对象的单元测试。 今天的博客探讨了有时被视为对立的技术&#xff1a;使用模拟对象进行单元测试。 同样&#xff0c;我使用了从数据库检索地址的简单方案&#xff1a;…