你在终端启动的进程,最后都是什么下场?(下)

news2024/7/6 19:20:14

你在终端启动的进程,最后都是什么下场?(下)

在上期文章你在终端启动的进程,最后都是什么下场?(上)当中我们介绍了前台进程最终结束的几种情况,在本篇文章当中主要给大家介绍后台进程号可能被杀死的几种情况。

揭秘nohup——后台进程的死亡

如果大家有过让程序在后台持续的运行,当你退出终端之后想让你的程序继续在后台运行,我们通常会使用命令 nohup。那么现在问题来了,为什么我们让程序在后台运行需要 nohup 命令,nohup 命令又做了什么?

在前面的文章你在终端启动的进程,最后都是什么下场?(上)当中我们已经谈到了,当你退出终端之后 shell 会发送 SIGHUP 信号给前台进程组的所有进程,然后这些进程在收到这个信号之后如果没有重写 SIGHUP 信号的 handler 或者也没有忽略这个信号,那么就会执行这个信号的默认行为,也就是退出程序的执行。

事实上当你退出终端之后 shell 不仅给前台进程组的所有进程发送 SIGHUP 信号,而且也会给所有的后台进程组发送 SIGHUP 信号,因此当你退出终端之后你启动的所有后台进程都会收到一个 SIGHUP 信号,注意 shell 是给所有的后台进程组发送的信号,因此如果你的后台进程是一个多进程的程序的话,那么你这个多进程程序的每一个进程都会收到这个信号。

根据上面的分析我们就可以知道了当我们退出终端之后,shell 会给后台进程发送一个 SIGHUP 信号。在我们了解了 shell 的行为之后我们应该可以理解为什么我么需要 nohup 命令,因为我们正常的程序是没有处理这个 SIGHUP 信号的,因此当我们退出终端之后所有的后台进程都会收到这个信号,然后终止执行。

看到这里你应该能够理解 nohup 命令的原理和作用了,这个命令的作用就是让程序忽略 SIGHUP 这个信号,我们可以通过 nohup 的源代码看出这一点。

nohup 的核心代码如下所示:

int
main(int argc, char *argv[])
{
	int exit_status;

	while (getopt(argc, argv, "") != -1)
		usage();
	argc -= optind;
	argv += optind;
	if (argc < 1)
		usage();

	if (isatty(STDOUT_FILENO))
		dofile();
	if (isatty(STDERR_FILENO) && dup2(STDOUT_FILENO, STDERR_FILENO) == -1)
		/* may have just closed stderr */
		err(EXIT_MISC, "%s", argv[0]);

	(void)signal(SIGHUP, SIG_IGN); // 在这里忽略 SIGHUP 这个信号

	execvp(*argv, argv); // 执行我们在命令行当中指定的程序
	exit_status = (errno == ENOENT) ? EXIT_NOTFOUND : EXIT_NOEXEC;
	err(exit_status, "%s", argv[0]);
}

在上面的程序当中我们可以看到,在 main 函数当中,nohup 首先创建使用 signal 忽略了 SIGHUP 信号,SIG_IGN 就是忽略这个信号,然后使用 execvp 执行我们在命令行当中指定的程序。

这里需要注意一点的是关于 execvp 函数,也就是 execve 这一类系统调用,只有当我们使用 SIG_IGN 忽略信号的时候,才会在 execvp 系列函数当中起作用,如果是我们自己定义的信号处理器 (handler),那么在我们执行完 execvp 这个系统调用之后,所有的我们自己定义的信号处理器的行为都将失效,所有被重新用新的函数定义的信号都会恢复成信号的默认行为。

比如说下面这个程序:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

void sig(int no)
{
  char* s = "Hello World\n";
  write(STDOUT_FILENO, s, strlen(s));
  sync();
}

int main(int argc, char* argv[], char* argvp[])
{

  signal(SIGINT, sig);
  execvp(argv[1], argv);
}

在上面的程序当中我们定义了一个信号处理器 sig 函数,如果接受到 SIGINT 信号那么就会执行 sig 函数,但是我们前面说了,因为只有 SIG_IGN 才能在 execvp 函数执行之后保持,如果是自定函数的话,那么这个信号的行为就会被重置成默认行为,SIGINT 的默认行为是退出程序,现在我们使用上面的程序去加载执行一个死循环的程序,执行结果如下:

从上面的程序的输出结果我们就可以知道,在我们按下 ctrl + c 之后进程会收到一个来自内核的 SIGINT 信号,但是并没有执行我们设置的函数 sig ,因此验证了我们在上文当中谈到的结论!

有心的同学可能会发现当我们在终端使用 nohup 命令的时候会生成一个 “nohup.out” 文件,记录我们的程序的输出内容,我们可以在 nohup 的源代码当中发现一点蛛丝马迹,我们可以看一下 nohup 命令的完整源代码:

#if 0
#ifndef lint
static const char copyright[] =
"@(#) Copyright (c) 1989, 1993\n\
	The Regents of the University of California.  All rights reserved.\n";
#endif /* not lint */

#ifndef lint
static char sccsid[] = "@(#)nohup.c	8.1 (Berkeley) 6/6/93";
#endif /* not lint */
#endif
#include <sys/cdefs.h>
__FBSDID("FreeBSD");

#include <sys/param.h>
#include <sys/stat.h>

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static void dofile(void);
static void usage(void);

#define	FILENAME	"nohup.out" // 定义输出文件的文件名
/*
 * POSIX mandates that we exit with:
 * 126 - If the utility was found, but failed to execute.
 * 127 - If any other error occurred. 
 */
#define	EXIT_NOEXEC	126
#define	EXIT_NOTFOUND	127
#define	EXIT_MISC	127

int
main(int argc, char *argv[])
{
	int exit_status;

	while (getopt(argc, argv, "") != -1)
		usage();
	argc -= optind;
	argv += optind;
	if (argc < 1)
		usage();

	if (isatty(STDOUT_FILENO))
		dofile();
	if (isatty(STDERR_FILENO) && dup2(STDOUT_FILENO, STDERR_FILENO) == -1)
		/* may have just closed stderr */
		err(EXIT_MISC, "%s", argv[0]);

	(void)signal(SIGHUP, SIG_IGN);

	execvp(*argv, argv);
	exit_status = (errno == ENOENT) ? EXIT_NOTFOUND : EXIT_NOEXEC;
	err(exit_status, "%s", argv[0]);
}

static void
dofile(void)
{
	int fd;
	char path[MAXPATHLEN];
	const char *p;

	/*
	 * POSIX mandates if the standard output is a terminal, the standard
	 * output is appended to nohup.out in the working directory.  Failing
	 * that, it will be appended to nohup.out in the directory obtained
	 * from the HOME environment variable.  If file creation is required,
	 * the mode_t is set to S_IRUSR | S_IWUSR.
	 */
	p = FILENAME;
  // 在这里打开 nohup.out 文件
	fd = open(p, O_RDWR | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
	if (fd != -1)
    // 如果文件打开成功直接进行文件描述符的替代,将标准输出重定向到文件 nohup.out 
		goto dupit;
	if ((p = getenv("HOME")) != NULL && *p != '\0' &&
	    (size_t)snprintf(path, sizeof(path), "%s/%s", p, FILENAME) <
	    sizeof(path)) {
		fd = open(p = path, O_RDWR | O_CREAT | O_APPEND,
		    S_IRUSR | S_IWUSR);
		if (fd != -1)
			goto dupit;
	}
	errx(EXIT_MISC, "can't open a nohup.out file");

dupit:
	if (dup2(fd, STDOUT_FILENO) == -1)
		err(EXIT_MISC, NULL);
	(void)fprintf(stderr, "appending output to %s\n", p);
}

static void
usage(void)
{
	(void)fprintf(stderr, "usage: nohup [--] utility [arguments]\n");
	exit(EXIT_MISC);
}

在源代码当中的宏 FILENAME 定义的文件名就是 nohup.out,在上面的代码当中,如果判断当前进程的标准输出是一个终端设备就会打开文件 nohup.out 然后将进程的标准输出重定向到文件 nohup.out ,因此我们在程序当中使用 printf 的输出就都会被重定向到文件 nohup.out 当中,看到这里就破案了,原来如此。

后台进程和终端的纠缠

后台进程是不能够从终端读取内容的,当我们从终端当中读的时候内核就会给这个后台进程发送一个 SIGTTIN 信号,这个条件主要是避免多个不同的进程都读终端。如果后台进程从终端当中进行读,那么这个进程就会收到一个 SIGTTIN 信号,这个信号的默认行为就是退出程序。

我们可以使用下面的程序进程测试:

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>


void sig(int no, siginfo_t* si, void* ucontext)
{
  char s[1024];
  sprintf(s, "signal number = %d sending pid = %d\n", no, si->si_pid);
  write(STDOUT_FILENO, s, strlen(s));
  sync();
  _exit(0);
}

int main()
{
  struct sigaction action;
  action.sa_flags |= SA_SIGINFO;
  action.sa_sigaction = sig;
  action.sa_flags &= ~(SA_RESETHAND);
  sigaction(SIGTTIN, &action, NULL);
  while(1)
  {
    char c = getchar();
  }
  return 0;
}

然后我们在终端输入命令,并且对应的输出如下:

➜  daemon git:(master) ✗ ./job11.out&
[1] 47688
signal number = 21 sending pid = 0                                                                                
[1]  + 47688 done       ./job11.out

从上面程序的输出结果我们可以知道,当我们在程序当中使用函数 getchar 读入字符的时候,程序就会收到来自内核的信号 SIGTTIN,根据下面的信号名和编号表可以知道,内核发送的信号位 SIGTTIN。

当我们在终端当中进行写操作的时候会收到信号 SIGTTOU,但是默认后台进程是可以往终端当中写的,如果我们想要进程不能够往终端当中写,当进程往终端当中写数据的时候就收到信号 SIGTTOU,我们可以使用命令 stty 进行设置。我们使用一个例子看看具体的情况:

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>


void sig(int no, siginfo_t* si, void* ucontext)
{
  char s[1024];
  sprintf(s, "signal number = %d sending pid = %d\n", no, si->si_pid);
  write(STDOUT_FILENO, s, strlen(s));
  sync();
  _exit(0);
}

int main()
{
  struct sigaction action;
  action.sa_flags |= SA_SIGINFO;
  action.sa_sigaction = sig;
  action.sa_flags &= ~(SA_RESETHAND);
  sigaction(SIGTTOU, &action, NULL);
  while(1)
  {
    sleep(1);
    printf("c");
    fflush(stdout);
  }
  return 0;
}

上面是一个比较简单的信号程序,不断的往终端当中输出字符 c,我们可以看一下程序的执行情况(job12 就是上面的代码):

➜  daemon git:(master) ✗ stty tostop 
➜  daemon git:(master) ✗ ./job12.out&
[1] 48467
➜  daemon git:(master) ✗ signal number = 22 sending pid = 0

[1]  + 48467 done       ./job12.out

在上面的输出结果当中我们使用命令 stty tostop 主要是用于启动当有后台进程往终端当中写内容的时候,向这个进程发送 SIGTTOU 信号,这个信号的默认行为也是终止进程的执行。

首先看一下当我们没有使用 stty tostop 命令的时候程序的行为。

现在我们使用 stty tostop 命令重新设置一下终端的属性,然后重新进程测试:

从上面的输出结果我们可以看到当我们在终端当中,默认是允许进程往终端当中进行输出的,但是当我们使用命令 stty tostop 之后,如果还有后台进程往终端当中进行输出,那么这个进程就会收到一个 SIGTTOU 信号。

后台进程和终端的命令交互

在前文当中我们谈到了当我们在一条命令后面加上 & 的话,那么这个程序将会变成后台进程。那么有没有办法将一个后台进程变成前台进程呢?

当然有办法,我们可以使用 fg ——一个 shell 的内置命令,将一个后台进程变成前台进程。在正式进行验证之前我们需要来了解三个命令:

  • jobs 这条命令主要是用于查看当前所有的后台进程组,也就是所有的后台作业,。
  • fg 这条命令主要是将一个后台进程放到前台来运行。
  • bg 这条命令主要是让一个终端的后台程序继续执行。

具体的例子如下所示:

➜  daemon git:(master)sleep 110 & # 创建一个后台进程 每当创建一个后台作业 shell 都会给这个作业分配一个作业号 就是 [] 当中的数字,从 1 开始
[1] 7467
➜  daemon git:(master)sleep 111 & # 创建一个后台进程
[2] 7485
➜  daemon git:(master)sleep 112 & # 创建一个后台即成
[3] 7503
➜  daemon git:(master)jobs # 查看所有的后台进程 其中 + 表示当前作业 可以认为是最近一次使用 & 生成的作业 - 表示上一个作业 可以认为是倒数第二个使用 & 生成的作业
[1]    running    sleep 110
[2]  - running    sleep 111
[3]  + running    sleep 112
➜  daemon git:(master)fg # fg 的使用方式为 fg %num 如果不指定 %num 的话,默认就是将当前作业放到前台 饿我们在上面已经谈到了 当前作业为 sleep 112 因此将这个进程恢复到前台
[3]  - 7503 running    sleep 112
^C # 终止这个作业
➜  daemon git:(master)jobs  # 因为终止了作业 sleep 112 因此后台进程组只剩下两个了
[1]    running    sleep 110
[2]  + running    sleep 111
➜  daemon git:(master)fg  # 在将最近一次提交的作业放到前台
[2]  - 7485 running    sleep 111
^C # 终止这个任务的执行
➜  daemon git:(master)sleep 112 &
[2] 7760
➜  daemon git:(master)jobs 
[1]  - running    sleep 110
[2]  + running    sleep 112
➜  daemon git:(master)sleep 112 &
[3] 7870
➜  daemon git:(master)sleep 112 &
[4] 7888
➜  daemon git:(master)jobs 
[1]    running    sleep 110
[2]    running    sleep 112
[3]  - running    sleep 112
[4]  + running    sleep 112
➜  daemon git:(master)fg %1 
[1]    7467 running    sleep 110
^C
➜  daemon git:(master)

接下来我们使用下面的程序进行验证,下面的程序的主要目的就是判断当前进程是否是前台进程,如果是则打印消息,如果不是那么就一直进行死循环:

#include <stdio.h>
#include <unistd.h>

int main()
{
  while(1)
  {
    sleep(1);
    // tcgetpgrp 返回前台进程组的进程组号
    // getpgid(0) 得到当前进程组的进程组号
    // 如果两个结果相等则说明当前进程组是前台进程组
    // 反之则是后台进程组
    if(getpgid(0) == tcgetpgrp(STDOUT_FILENO))
    {
      printf("I am a process of foregroup process\n");
    }
  }
  return 0;
}

然后我们在终端当中执行这个程序,对应的几个结果如下所示:

➜  daemon git:(master) ✗ ./job13.out& # 先将这个程序放到后台运行,因为不是前台程序因此不会打印消息
[1] 5832
➜  daemon git:(master)fg    # 将这个程序放到前台执行,因为到了前台因此上面的程序会输出消息
[1]  + 5832 running    ./job13.out
I am a process of foregroup process
I am a process of foregroup process
I am a process of foregroup process
^Z
[1]  + 5832 suspended  ./job13.out # 在这里我们按下 ctrl + z 给进程发送 SIGTSTP 信号 让进程暂停执行
➜  daemon git:(master)bg %1 # bg 命令默认是给进程发送一个 SIGCONT 因为在上一行当中信号 SIGTSTP 让进程暂停执行了 因此进程在收到信号 SIGCONT 之后会继续执行(SIGCONT 的作用就是让一个暂停的进程继续执行)
[1]  + 5832 continued  ./job13.out # 因为进程还是在后台当中,因此进程继续执行还是在后台执行,所以依然没有输出
➜  daemon git:(master)fg %1 # 这条命令是让后台进程组当中的第一个作业到前台执行,因此进程开始打印输出
[1]  + 5832 running    ./job13.out
I am a process of foregroup process
I am a process of foregroup process
^C # 在这里输入 ctrl + c 命令,让前台进程组当中所有进程停止执行
➜  daemon git:(master)

在上面的输出结果当中,我们首先在后台启动一个进程,因为是在后台所以当前进程组不是前台进程组,因此不会在终端当中打印输出,而当我们使用 fg 命令将后台当中的最近生成的一个作业(当我们输入命令之后,终端打印的[]当中的数字就是表示作业号,默认是从 1 开始的,因为我们只启动一个后台进程(执行一条命令就是开启一个作业),因此作业号等于 1)放到前台来执行,在上面的例子当中,命令 fg 和 fg %1 的效果是一样的。

总结

在本篇文章当中主要给大家介绍了后台进程的一些生与死的情况,总体来说有以下内容:

  • 当我们退出终端的时候,shell 会给所有前台和后台进程组发送一个 SIGHUP 信号,nohup 命令的原理就是让程序忽略这个 SIGHUP 信号。
  • 当后台进程从终端当中读的时候内核会给这个进程发送一个 SIGTTIN 信号。
  • 当我们设置了 ssty tostop 之后,如果我们往终端当中进行写操作的话,那么内核会给这个进程发送一个 SIGTTOU 信号,这两个信号的默认行为都是终止这个进程的执行。
  • 我们可以使用 jobs fg bg 命令让终端和后台进程进行交互操作,fg 将一个后台进程放到前台执行,如果这个进行暂停执行的话, shell 还会给这个进程发送一个 SIGCONT 信号让这个进程继续执行,bg 可以让一个后台暂停执行的进程恢复执行,本质也是给这个后台进程发送一个 SIGCONT 信号。
  • 当你在终端当中输入 ctrl + c 的时候,内核会给所有的前台进程组当中所有的进程发送 SIGINT 信号,当你在终端输入 ctrl + z 时,内核会给前台进程组当中的所有进程发送 SIGTSTP 信号,当你在终端输入 ctrl + \ 内核会给所有的前台进程组发送 SIGQUIT 信号。
  • 综合上面的分析,上面的结果可以使用下面的图进行表示分析。

以上就是本篇文章的所有内容了,我是LeHung,我们下期再见!!!更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

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

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

相关文章

好书分享丨区块链的骨骼——密码技术

开放隐私计算 开放隐私计算 开放隐私计算OpenMPC是国内第一个且影响力最大的隐私计算开放社区。社区秉承开放共享的精神&#xff0c;专注于隐私计算行业的研究与布道。社区致力于隐私计算技术的传播&#xff0c;愿成为中国 “隐私计算最后一公里的服务区”。 180篇原创内容 …

darknet框架GPU编译安装

Darknet: Open Source Neural Networks in C 1、darknet下载 git clone https://github.com/pjreddie/darknet.git cd darknet设置makefile gpu1 cudnn1 opencv1【1】GPU1;需要设置显卡驱动、cuda 使用nvidia-smi 查看显卡型号和支持的cuda版本号 nvidia官网下载cuda,以及…

计算机网络学习笔记(Ⅱ):物理层

目录 1 物理层概念 1.1 物理层基本概念 1.定义 2.主要任务 3.特性 1.2 数据通信基础 1.典型模型 2.相关术语 3.三种通信方式 4.数据传输方式 1.3 物理层内容 1.码元 2.速率 3.带宽 1.4 奈氏准则与香农定理 1.失真 2.码间串扰 3.奈氏准则 4.香农定理 1.5 …

蓝桥杯C/C++VIP试题每日一练之Huffman树

💛作者主页:静Yu 🧡简介:CSDN全栈优质创作者、华为云享专家、阿里云社区博客专家,前端知识交流社区创建者 💛社区地址:前端知识交流社区 🧡博主的个人博客:静Yu的个人博客 🧡博主的个人笔记本:前端面试题 个人笔记本只记录前端领域的面试题目,项目总结,面试技…

基于JSP的某餐厅点餐系统

目 录 第一章 绪论 1 1.1系统研究背景和意义 1 1.2研究现状 1 1.3研究主要内容 2 第二章 相关技术说明 3 2.1 JSP(Java Server Page)简介 3 2.2 Spring框架简介 4 2.3 Spring MVC框架简介 5 2.4 MyBatis 框架简介 5 2.4 MySql数据库简介 6 2.6 Tomcat简介 7 2.7 jQuery简介 8 …

Hadoop原理与技术——Hbase的基本操作

点击链接查看文档 一、实验目的 上机实操&#xff0c;熟悉指令操作Hbase和java代码操作Hbase 二、实验环境 Windows 10 VMware Workstation Pro虚拟机 Hadoop环境 Jdk1.8 三、实验内容 1&#xff1a;指令操作Hbase (1)&#xff1a;start-all.sh&#xff0c;启动所有进程 (2)…

Ansys(Maxwell、Simplorer)与Simulink联合仿真(二)直线电机

Ansys&#xff08;Maxwell、Simplorer&#xff09;与Simulink联合仿真&#xff08;二&#xff09;直线电机 在仿真过程中&#xff0c;遇到了一个问题&#xff0c;卡了好久得到了解决。 关于 motion setup 提示 moving 找不到面 cannot find the sarface 所有的动态部件要隔开…

【pen200-lab】10.11.1.21(实际获得22权限)

pen200-lab 学习笔记 【pen200-lab】10.11.1.21 &#x1f525;系列专栏&#xff1a;pen200-lab &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f4c6;首发时间&#xff1a;&#x1f334;2022年11月27日&#x1f334; &#x1f36d;作…

算法导论24章单源最短路径—Bellman-Ford算法 Dijkstra算法

松弛操作 松弛操作就是判断从现在s到v的路径更近&#xff0c;还是我从s到u再到v更近&#xff0c;选一个更近的走。 松弛操作的例子 松弛是唯一导致最短路径估计和前驱结点变化的操作 Bellman-Ford算法 第一个循环&#xff0c;循环V-1次&#xff0c;每次循环对所有的边都松弛一…

Python数据分析-matplotlib

目录 一、折线图&#xff1a;plt.plot() 1.1 plt.plot()基本用法 1.2 设置坐标轴范围&#xff1a;plt.axis([xmin,xmax,ymin,ymax]) 1.3 plt.plot()绘制多个图形 1.4 linewidth设置线条宽度 1.5 使用plt.plot()的返回值设置线条属性 1.6 plt.setp()修改线条性质 1.7 对…

软件测试的几种方法

1、从是否关心内部结构来看 (1)白盒测试&#xff1a;又称为结构测试或逻辑驱动测试&#xff0c;是一种按照程序内部逻辑结构和编码结构&#xff0c;设计测试数据并完成测试的一种测试方法。 (2)黑盒测试&#xff1a;又称为数据驱动测试&#xff0c;把测试对象当做看不见的黑盒…

讲透金融风控建模全流程(附 Python 代码)

信贷风控是数据挖掘算法最成功的应用之一&#xff0c;这在于金融信贷行业的数据量很充足&#xff0c;需求场景清晰及丰富。 信贷风控简单来说就是判断一个人借了钱后面&#xff08;如下个月的还款日&#xff09;会不会按期还钱。更专业来说&#xff0c;信贷风控是还款能力及还…

SQL 汇总统计及GROUP BY

SQL 汇总统计1、汇总统计2、GROUT BY3、如何对分组统计的结果进行过滤&#xff1f; GROUP BY HAVING4、如何对分组统计的结果进行排序&#xff1f;GROUP BY ORDER BY5、介绍SELECT语句中各个子句的书写顺序6、备注&#xff1a; 上方用到的表1、汇总统计 介绍几个聚集函数 有…

电脑误删Path环境变量后前端如何重新配置所需变量

需求背景 &#xff1a; 当时公司需要我们安装一款软件 &#xff0c; 按照操作文档需要配置一下 Path 环境变量 &#xff0c; 但当时的云桌面操作系统是 window7系统 &#xff0c; 当时配置时并不知道新的变量配置时需要在前面一个的后面加 “ &#xff1b; ” 来间隔开来…

【目的:windows下VS2017/2022使用MSVC编译GLFW库】

目的&#xff1a;windows下VS2017/2022使用MSVC编译GLFW库 环境&#xff1a; 系统&#xff1a;Win10 环境&#xff1a;VS2017 64bit步骤&#xff1a; 1.下载GLFW源码 官网链接https://www.glfw.org/download.html&#xff0c; 下载glfw的源码&#xff0c;解压到本地&#x…

考研数据结构大题整合_组二(TJP组)

考研数据结构大题整合 目录考研数据结构大题整合二、TJP组TJP组一TJP组二TJP组三二、TJP组 TJP组一 四、画图/计算/证明/算法分析&#xff08;30分&#xff09; &#xff08;1&#xff09;证明题&#xff08;8分&#xff09; 如果一棵树有n1个度为1的结点&#xff0c;n2个度为…

(四)Vue之数据绑定

文章目录数据绑定单向数据绑定双向数据绑定Vue学习目录上一篇&#xff1a;&#xff08;三&#xff09;Vue之模板语法 数据绑定 Vue中有2种数据绑定的方式&#xff1a; 1.单向绑定&#xff1a;数据只能从data流向页面。2.双向绑定&#xff1a;数据不仅能从data流向页面&#…

著名书画家、中国书画院院士李适中

著名书画家、中国书画院院士李适中 李适中 著名书画家、中国书画院院士 版画艺术家 文物复制专家 中国文物学会会员单位创始人 文化部科技进步奖获得者 艺术简历 李适中&#xff0c;1943年生&#xff0c;安徽颍上人&#xff0c;著名书画家、中国书画院院士。李适中先生师从著名…

Vue3+nodejs全栈项目(资金管理系统)——前端篇

文章目录创建项目项目初始化使用element-plus设置Register和404组件搭建element注册表单验证表单和按钮加载动画和消息提醒路由守卫和token过期处理配置请求拦截和响应拦截解析token并存储到vuex中设计顶部导航设置首页和个人信息设置左侧导航栏展示资金管理页面添加按钮编辑和…

返回当前系统串口名称

主要针对当前的usb转串口进行了穷举。 方便判断串口对应哪个设备。 返回串口名称 类对象&#xff0c;&#xff08;包含了参考网址&#xff0c;以及对其进行了修改&#xff0c;防止出现蓝牙端口&#xff09; using System; using System.Collections.Generic; using System.L…