Linux系统编程 day07 信号

Linux系统编程 day07 信号

  • 1. 信号的介绍以及信号的机制
  • 2. 信号相关函数
    • 2.1 `signal`
    • 2.2 `kill`
    • 2.3 `abort`和`raise`
    • 2.4 `alarm`
    • 2.5 `setitimer`
  • 3. 信号集
  • 4. 信号捕捉函数
  • 6. `SIGCHLD`信号
  • 7. `SIGUSR1`与`SIGUSR2`

1. 信号的介绍以及信号的机制

信号是信息的载体,在Linux/Unix环境下,信号是一种很重要的通信手段。信号通信的方式简单,但是不能携带大量的信息,信号是满足某个特定的条件才会产生的。

在Linux中,进程A给进程B发送信号,进程B收到信号之前执行自己的代码,收到信号后,不管执行到应用程序的什么位置,都要暂停运行,去处理信号,处理完毕后再继续执行。信号是在软件的层面上实现的中断,早期被称为软中断。每一个进程收到的信号都是由内核负责发送的。信号的发送过程如下:

在这里插入图片描述
信号的状态有三种,分别是产生、未决、递达。信号的产生由以下的几种方式:

  • 按键产生,如:Ctrl + C(SIGINT)、Ctrl + Z (SIGSTOP)、Ctrl + \ (SIGTERM)。
  • 系统调用产生,如:调用kill raiseabort
  • 软件条件产生,如:定时器alarm
  • 硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)。
  • 命令产生,如:kill命令。

信号的未决状态时是信号产生和信号递达之间的状态,主要是由于阻塞导致了该状态,后面我们会用sigpromask去对某个信号集进行阻塞等操作。信号的递达是信号递达并且到达该进程。

进程收到了信号之后,就会对信号进行相应的处理。信号处理默认动作有以下几种方式:

在这里插入图片描述

处理方式处理结果
Term默认行为是终止进程
Ign默认行为是忽略信号
Core默认行为是终止进程并生成core文件
Stop默认行为是暂停进程
Cont默认行为是如果进程当前状态为暂停则继续运行进程

当然,除了默认处理之外我们还可以捕捉信号调用用户的自定义处理函数。由于信号的发送是需要由用户态切换到内核态,处理也是需要进入内核态,所以用信号这种实现进程间通信的手段会导致信号有很强的延时性。虽然有延时,但是这个时间对于用户来说是非常短的,用户基本上察觉不到。

进程的相关信息存储在PCB中,在PCB中有阻塞信号集和未决信号集。阻塞信号集的作用是对信号进行阻塞,若一个信号被阻塞之后如果收到了该信号就会留在未决信号集中,不会被处理。未决信号集是用于存储需要处理的信号,这些信号因为某些原因不能抵达,在解决屏蔽之前,信号都是一直处于未决状态。当信号被处理,这信号会从未决信号集中消失。

信号有四要素,分别是信号的编号、信号的名称、产生信号的事件和默认的处理动作。其中,信号编号为1~31之间的信号为常规信号,也称为普通信号或者标准信号。34~64之间的信号称为实时信号,与驱动编程和硬件相关。下面是信号的四要素表:

在这里插入图片描述在这里插入图片描述
信号编号表:

在这里插入图片描述

从上面可以看到这些信号的编号在不同的操作系统架构中是不一样的,因此我们使用信号的时候应该使用信号的名称。在上面的所有信号之中,特别需要注意SIGKILLSIGSTOP信号是不能被捕获和阻塞或者是忽略的。

在系统编程中,我们常用的信号有SIGINTSIGQUITSIGKILLSIGSEGVSIGUSR1SIGUSR2SIGPIPESIGALRMSIGTERMSIGCHLDSIGSTOPSIGCONT

这些信号的含义如下:

常用信号含义
SIGINT按键中断
SIGQUIT退出
SIGKILL杀死
SIGSEGV内存溢出非法操作内存等
SIGUSR1用户使用自定义信号
SIGUSR2用户使用自定义信号
SIGPIPE管道破裂
SIGALRM定时器信号
SIGTERM终止信号
SIGCHLD子进程死亡、暂停、继续时给父进程发送的信号
SIGSTOP暂停进程
SIGCONT继续运行暂停的进程

2. 信号相关函数

2.1 signal

该函数用于注册信号捕捉函数,函数的原型如下:

#include <signal.h>

// 信号处理函数类型
typedef void (*sighandler_t)(int);

// 作用: 注册信号捕捉函数
// 返回值: 以前信号的处理函数指针
// 参数: signum: 信号编号
//       handler: 信号处理函数
sighandler_t signal(int signum, sighandler_t handler);

该函数的使用示例如下:

// signal函数测试--注册信号处理函数
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

// 信号处理函数
void sighandler(int signo)
{
	printf("signo = [%d]\n", signo);
}

int main()
{
	// 注册信号处理函数
	signal(SIGINT, sighandler);

	sleep(200);

	return 0;
}

需要注意的是signal函数的行为在不同的UNIX版本之间是不同的,在不同的Linux版本之间也是不同的,应该避免是用它,后续会用sigaction函数取代。

2.2 kill

kill函数用于给指定的进程发送指定的信号。该函数的原型为:

#include <sys/types.h>
#include <signal.h>

// 作用: 给指定进程发送指定信号
// 返回值: 成功返回0,失败返回-1并设置errno
// 参数:  pid: 进程号
//             pid > 0: 发送信号给指定进程。
//             pid = 0: 发送信号给调用kill进程属于同一进程组的
//                      所有进程。
//             pid < -1: 发送给pid绝对值对应的进程组。
//             pid = -1: 发送给进程有权限发送的系统中所有进程。
//        sig: 信号
int kill(pid_t pid, int sig);

函数中的sig信号参数不推荐使用数字,应该使用宏名,因为不同的操作系统信号编号可能不一样,但名称不一样。

在Linux中,每一个进程都属于一个进程组,进程组是一个或者多个进程的集合,他们是相互关联的,共同完成一个实体任务,每一个进程都有一个进程组长,默认进程组ID与进程组组长ID相同。

关于kill的示例代码如下:

// kill函数测试--发送信号
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

// 信号处理函数
void sighandler(int signo)
{
	printf("signo = [%d]\n", signo);
	kill(getpid(), SIGKILL);
}

int main()
{
	// 注册信号处理函数
	signal(SIGINT, sighandler);

	while(1)
	{
		sleep(1);
		kill(getpid(), SIGINT);
	}

	return 0;
}

2.3 abortraise

这两个函数是进程自己给自己发送信号,其中abort用于给自己发送异常终止信号SIGABRT,并会产生core文件,而raise函数是用于给自己发送指定信号。这两个函数的原型如下:

#include <signal.h>

// 作用: 给当前进程发送指定信号,自己给自己发
// 返回值: 成功返回0,失败返回非0值
// 参数: sig: 信号编号
int raise(int sig);
#include <stdlib.h>

// 作用: 给自己发送异常终止信号SIGABRT,并产生core文件
void abort(void);

这两个函数的功能都可以使用kill函数实现。这两个函数的示例代码如下:

raise函数测试

// raise函数测试 -- 给自己发送指定信号
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

// 信号处理函数
void sighandler(int signo)
{
	printf("signo = [%d]\n", signo);
	raise(SIGKILL);
}

int main()
{
	// 注册信号处理函数
	signal(SIGINT, sighandler);
	
	while(1)
	{
		raise(SIGINT);
	}

	return 0;
}

abort函数测试

// abort函数测试 -- 给自己发送异常终止信号
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

// 注册信号处理函数
void sighandler(int signo)
{
	printf("signo = [%d]\n", signo);
	raise(SIGSTOP);
}

int main()
{
	// 注册信号处理函数
	signal(SIGABRT, sighandler);

	abort();
	
	return 0;
}

2.4 alarm

该函数主要是用于设置定时器。函数的原型如下:

#include <unistd.h>

// 作用: 设置定时器(闹钟),在指定seconds之后,内核会给当前进程发送
//       SIGALRM信号。进程收到该信号之后,默认动作为终止。每一个进程
//       都有且只有唯一的一个定时器。
// 返回值: 返回0或者剩余的秒数,无失败的返回
// 参数: seconds: 秒数
unsigned int alarm(unsigned int seconds);

该函数的如果把参数设置为0就表示取消定时器,也就是alarm(0)alarm函数使用的是自然定时,与进程的状态无关,无论进程处于什么状态alarm都计时。该函数的示例代码如下:

// alarm函数测试--设置定时器
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

int main()
{
	// 设置定时器
	int ret = alarm(5);
	printf("ret = [%d]\n", ret);
	
	sleep(2);

	ret = alarm(4);
	printf("ret = [%d]\n", ret);

	sleep(1);

	ret = alarm(4);
	printf("ret = [%d]\n", ret);

	while(1)
	{
		sleep(1000);
	}

	return 0;
}

2.5 setitimer

setitimer函数也是用于设置定时器的,可以代替alarm函数。该函数的精确到微妙,可以实现周期定时。函数的原型如下:

#include <sys/time.h>

// 作用: 设置定时器,精度为微妙,可以实现周期定时。
// 返回值: 成功返回0,失败返回-1并设置errno。
// 参数: which: 定时方式。
//       new_value: 负责设定timeout时间
//       old_value: 存放旧的timeout时间,一般指定为NULL。 
int setitimer(int which, const struct itimerval *new_value,
                    struct itimerval *old_value);

// 参数的结构体
struct itimerval {
       struct timeval it_interval; // 闹钟触发周期
       struct timeval it_value;    // 闹钟第一次触发时间
};

struct timeval {
       time_t      tv_sec;         // 秒
       suseconds_t tv_usec;        // 微妙
};

其中定时方式主要有以下几种:

定时方式宏名信号含义
自然定时ITIMER_REALSIGALRM计算自然时间
虚拟空间计时(用户空间)ITIMER_VIRTUALSIGVTALRM只计算占用CPU的时间
运行时计时(用户 + 内核)ITIMER_PROFSIGPROF计算占用CPU以及执行系统调用的时间

该函数的使用示例如下:

// setitimer函数测试
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>
#include <signal.h>

// 信号处理函数
void sighandler(int signo)
{
	printf("signo = [%d]\n", signo);
}

int main()
{
	// 注册信号处理函数
	signal(SIGALRM, sighandler);

	// 设置周期时间
	struct itimerval tm;
	tm.it_interval.tv_sec = 1;
	tm.it_interval.tv_usec = 0;
	tm.it_value.tv_sec = 3;
	tm.it_value.tv_usec = 0;

	setitimer(ITIMER_REAL, &tm, NULL);

	while(1)
	{
		sleep(1);
	}

	return 0;
}

3. 信号集

信号集主要是两个信号集。第一个是未决信号集,该信号集存放了当前进程要阻塞的信号。第二个是阻塞信号集,该信号集存放了当前进程中还处于未决状态的信号。这两个集合都存储在PCB中。

在一个进程运行的时候,如果该进程收到了一个信号A,则这个信号首先会被保存到未决信号集中对应的信号编号的存储位置,将该位置置为1,此时该信号处于未决状态。当要处理这个信号的时候,会去检查阻塞信号集中对应的信号编号的存储位置上是否为0,若为1表示需要阻塞该信号,则信号不会被处理,若为0则表示该信号不被阻塞。当未决信号集的信号没有被阻塞被处理之后,未决信号集的信号编号对应的存储位置会被重置为0。

信号集在Linux中使用sigset_t进行定义。该类型是一个结构体,结构体定义如下:

#ifndef ____sigset_t_defined
#define ____sigset_t_defined

#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
  unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;

#endif

由于信号集属于内核的一块区域,所以用户不能直接操作内核空间,因此我们需要使用信号集相关的一些接口函数完成对信号集的相关操作。

接下来看一些信号集的相关函数,信号集相关函数原型如下:

#include <signal.h>

// 作用: 将某个信号集清0
// 返回值: 成功返回0,失败返回-1并设置errno
// 参数: set: 信号集
int sigemptyset(sigset_t *set);

// 作用: 将某个信号集置1
// 返回值: 成功返回0,失败返回-1并设置errno
// 参数: set: 信号集
int sigfillset(sigset_t *set);

// 作用: 将某个信号加入到信号集中
// 返回值: 成功返回0,失败返回-1并设置errno
// 参数: set: 信号集
//      signum: 信号编号
int sigaddset(sigset_t *set, int signum);

// 作用: 将某个信号清出信号集
// 返回值: 成功返回0,失败返回-1并设置errno
// 参数: set: 信号集
//      signum: 信号编号
int sigdelset(sigset_t *set, int signum);

// 作用: 判断某个信号是否在信号集中
// 返回值: 存在返回1,不存在返回0,失败返回-1并设置errno
// 参数: set: 信号集
//      signum: 信号编号
int sigismember(const sigset_t *set, int signum);

#include <signal.h>

// 作用: 用于屏蔽信号,解除屏蔽信号
// 返回值: 成功返回0,失败返回-1并设置error
// 参数: how: 参数取值决定函数的作用,how的取值有以下:
//              SIG_BLOCK: 设置屏蔽信号,set为需要屏蔽的信号
//              SIG_UNBLOCK: 设置解除屏蔽信号,set表示要解除屏蔽信号
//              SIG_SETMASK: set用于代替原始屏蔽集的新屏蔽集
//       set: 需要设置的信号集
//       oldset: 在设置操作之前的信号集 
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
#include <signal.h>

// 作用: 读取当前进程的未决信号集
// 返回值: 成功返回0,失败返回-1并设置errno
// 作用: set: 信号集
int sigpending(sigset_t *set);

接下来使用一个例子来说明以下信号集相关函数的使用,该例子的作用是将当前进程未决信号集中的常用信号集打印出来。

// 信号集相关函数测试
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <signal.h>
#include <unistd.h>

// 信号处理函数
void sighandler(int signo)
{
	printf("signo = [%d]\n", signo);
}

int main()
{
	// 注册信号处理函数
	signal(SIGINT, sighandler);
	signal(SIGQUIT, sighandler);

	// 定义信号集
	sigset_t set; // 设置的信号集
	sigset_t pending; // 未决信号集
	sigset_t oldset; // 保存之前的信号集

	// 初始化信号集
	sigemptyset(&set);
	sigemptyset(&oldset);
	sigemptyset(&pending);

	// 将SIGINT和SIGQUIT加入到信号集中
	sigaddset(&set, SIGINT);
	sigaddset(&set, SIGQUIT);

	// 将SIGINT和SIGQUIT加入到阻塞信号集中
	// sigprocmask(SIG_BLOCK, &set, NULL);
	sigprocmask(SIG_BLOCK, &set, &oldset);

	int j = 1;

	while(1)
	{
		// 获取未决信号集
		sigpending(&pending);

		for(int i = 1; i < 32; i ++)
		{
			// 判断某个信号是否位于信号集中
			if(sigismember(&pending, i))
			{
				printf("1");
			}
			else 
			{
				printf("0");
			}
		}
		printf("\n");

		if(j ++ % 10 == 0)
		{
			// 解除阻塞
			// sigprocmask(SIG_UNBLOCK, &set, NULL);
			sigprocmask(SIG_UNBLOCK, &set, &oldset);
		}
		else
		{
			// 阻塞
			sigprocmask(SIG_BLOCK, &set, NULL);
		}

		sleep(1);

	}

	return 0;
}

程序的运行结果如下:

在这里插入图片描述

4. 信号捕捉函数

这里的信号处理函数是说的sigaction,该函数的作用和signal一样。不同的signal函数在不同的Unix或者Linux下其行为可能是不一样的,而sigaction的行为都是一样的,想要做一个跨平台的程序一般使用sigaction函数。该函数的原型如下:

#include <signal.h>

// 作用: 注册一个信号处理函数
// 返回值: 成功返回0,失败返回-1并设置errno
// 参数: signum: 捕捉的信号
//       act: 新的处理方式
//       oldact: 旧的处理方式
int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);

// struct sigaction 定义如下
struct sigaction {
    // 信号处理函数
    void     (*sa_handler)(int); 
    // 信号处理函数,一般不用这个
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    // 处理期间需要阻塞的信号
    sigset_t   sa_mask;
    // 默认标识,通常设置为0
    int        sa_flags;
    // 已丢弃不用
    void     (*sa_restorer)(void);
};

在上面结构体的sa_handler,表示指定信号捕捉后处理的函数名,也可以赋值为SIG_IGN表示忽略,SIG_DFL表示执行默认动作。sa_mask用于指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理的时候,它自身会被自动放入到进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生。需要注意的是sa_mask只在处理函数调用期间屏蔽生效,是一个临时性的设置。另外需要知道的是信号是不支持排队的。也就是在一个信号处理函数执行期间该函数接收到了很多个该信号,则信号处理函数处理完该信号之后只执行一次处理。

内核再执行信号处理函数的过程如下:

在这里插入图片描述
接下来给一个关于使用sigaction函数的例子。

// sigaction函数测试:信号捕获
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

// 信号处理函数
void sighandler(int signo)
{
	printf("signo = [%d]\n", signo);
	sleep(5);
}

int main()
{
	// 注册信号处理函数
	struct sigaction act;
	act.sa_handler = sighandler; // 信号处理函数
	sigemptyset(&act.sa_mask);
	sigaddset(&act.sa_mask, SIGQUIT);  // 信号执行期间需要阻塞的信号
	act.sa_flags = 0;
	sigaction(SIGINT, &act, NULL); // 注册信号处理函数

	signal(SIGQUIT, sighandler);

	while(1)
	{
		sleep(1);
	}
	
	return 0;
}

6. SIGCHLD信号

SIGCHLD信号主要是由子进程在一定条件下触发并由内核给父进程发送的信号,该信号的产生有三种情况:

  • 子进程结束的时候。
  • 子进程收到SIGSTOP信号。
  • 子进程停止时收到SIGCONT信号。

子进程退出之后,内核会给父进程发送SIGCHLD信号,因此我们可以用这个信号完成对子进程的回收。这样避免父进程一直阻塞等待而不能执行其它操作。在回收子进程的时候,我们需要考虑到有可能还没有完成信号注册的时候子进程就可能退出了,也有可能在SIGCHLD处理期间SIGCHLD再次产生了很多次,若不进行多次回收则会出现僵尸进程。

避免第一个问题的方法是在注册处理函数之前先对SIGCHLD信号进行阻塞;第二个问题的解决是使用循环进行多次回收。因此我们可以写出来以下用SIGCHLD函数完成子进程回收的例子。关于信号产生的条件可以自行验证。

// 使用SIGCHLD回收子进程的资源
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

// 注册信号处理函数
void sighandler(int signo)
{
	while(1)
	{
		pid_t wpid = waitpid(-1, NULL, WNOHANG);
		if(wpid > 0)
		{
			printf("Child pid = [%d] is dead\n", wpid);
		}
		else if(wpid == 0)
		{
			printf("Child is living\n");
			break;
		}
		else if(wpid == -1)
		{
			printf("No child is living\n");
			break;
		}
	}
}

int main()
{
	// 在注册SIGCHLD之前阻塞SIGCHLD信号
	sigset_t  set;
	sigemptyset(&set);
	sigaddset(&set, SIGCHLD);
	sigprocmask(SIG_BLOCK, &set, NULL);
	
	// 创建子进程
	int i;
	for(i = 0; i < 3; i ++)
	{
		pid_t pid = fork();
		if(pid < 0)
		{
			perror("fork error");
			return -1;
		}
		else if(pid == 0)
		{
			printf("Child: pid = [%d], fpid = [%d]\n", getpid(), getppid());
			break;
		}

	}
	
	if(i == 0)
	{
		printf("I am child [%d]\n", getpid());
		sleep(2);
	}
	else if(i == 1)
	{
		printf("I am child [%d]\n", getpid());
		sleep(1);
	}
	else if(i == 2)
	{
		printf("I am child [%d]\n", getpid());
		sleep(2);
	}
	else if(i == 3)
	{
		sleep(4);
		// 注册信号处理函数
		struct sigaction act;
		act.sa_handler = sighandler;
		sigemptyset(&act.sa_mask);
		act.sa_flags = 0;
		sigaction(SIGCHLD, &act, NULL);

		// 取消阻塞
		sigprocmask(SIG_UNBLOCK, &set, NULL);

	}

	return 0;
}

7. SIGUSR1SIGUSR2

在系统编程的时候,我们不能随便乱使用kill函数去对某个进行发送一个系统的信号,因此操作系统提供用户两个信号使用。接下来我们使用这个两个信号完成两个进程交替数数。示例代码如下:

// 使用SIGUSR实现交替数数
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

int num = 0;
int flag;

// 信号处理函数
void sigusr1handler(int signo)
{
	printf("USR1: [%d]\n", num);
	num += 2;
	flag = 0;
	sleep(1);
}

void sigusr2handler(int signo)
{
	printf("USR2: [%d]\n", num);
	num += 2;
	flag = 0;
	sleep(1);
}

int main()
{
	// 创建子进程
	pid_t pid = fork();
	if(pid < 0)
	{
		perror("fork error");
		return -1;
	}
	else if(pid == 0)
	{
		// 注册信号处理函数
		struct sigaction act;
		act.sa_handler = sigusr2handler;
		sigemptyset(&act.sa_mask);
		act.sa_flags = 0;
		sigaction(SIGUSR2, &act, NULL);

		num = 2;
		flag = 0;
		while(1)
		{
			if(flag == 0)
			{
				kill(getppid(), SIGUSR1);
				flag = 1;
			}
		}
	}
	else 
	{
		// 注册信号处理函数
		struct sigaction act;
		act.sa_handler = sigusr1handler;
		sigemptyset(&act.sa_mask);
		act.sa_flags = 0;
		sigaction(SIGUSR1, &act, NULL);

		num = 1;
		flag = 1;
		while(1)
		{
			if(flag == 0)
			{
				kill(pid, SIGUSR2);
				flag = 1;
			}
		}
	}

	return 0;
}

运行结果为:

在这里插入图片描述
在上面的代码之中,我们分别通过信号处理函数来控制while循环中的if语句是否执行。两个进程分别互相发送信号就可以完成进程的交替输出。由于flag变量和num变量需要在信号处理函数中能够访问,所以定义成了全局变量。具体的运行过程可以通过分析程序结构理解理解。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

行业追踪,2023-11-30

自动复盘 2023-11-30 凡所有相&#xff0c;皆是虚妄。若见诸相非相&#xff0c;即见如来。 k 线图是最好的老师&#xff0c;每天持续发布板块的rps排名&#xff0c;追踪板块&#xff0c;板块来开仓&#xff0c;板块去清仓&#xff0c;丢弃自以为是的想法&#xff0c;板块去留让…

企业数字化转型应对传统网络挑战的关键策略

数字化变革正在以前所未有的速度和规模改变着我们的生活和工作方式&#xff0c;使得传统网络架构面临着巨大的挑战。其中包括带宽需求增加、多云应用增加、安全威胁增加以及传统网络设备无法满足需求等问题。 数字化时代需要更高速、更可靠、更安全的网络支持&#xff0c;传统网…

C语言必备知识--函数返回局部变量

0.总结 1. 不能以局部变量的方式创建字符串数组的首地址 2.如果函数的返回值非要是一个局部变量的地址&#xff0c;那么该局部变量一定要申明为static类型 3.返回指向字符串常量的指针 4.数组不能作为函数返回值 5.在函数中可以返回局部变量的值&#xff0c;但是不能返回…

如何安装鸿蒙Harmony 4.0低版API9三方库

比如我要用下拉刷新三方库pulltorefresh 安装命令如下 ohpm install ohos/pulltorefresh 安装完后然后运行Demo报错,说没有isAtEnd方法 然后查看pulltorefresh 最新版2.0.4对应Harmony API10,然而我的手机是API9,所以必须找到API9的库&#xff0c;然后查看2.0.1是还是API9 所…

docker集群的详解以及超详细搭建

文章目录 一、问题引入1. 多容器位于同一主机2. 多容器位于不同主机 二、介绍三、特性四、概念1. 节点nodes2. 服务(service)和任务(task)3. 负载均衡 五、docker网络1. overlay网络 六、docker集群搭建1. 环境介绍2. 创建集群3. 集群网络4. 加入工作节点 七、部署可视化界面po…

建设中国版MBA在线教育网站,群硕为Quantic敲开中国大门

2024考研即将拉开序幕&#xff0c;一个令人胆寒的问题出现在问答社区热榜—— 从现实来看&#xff0c;学历贬值已经成为一种全球现象。在卷学历的也不仅是大学生&#xff0c;还有很多职场人士&#xff0c;渴望通过获得MBA学位成为精英人才、商业领袖。 Quantic是交互式MBA线上…

热部署怎么部署

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言操作流程&#xff1a;在这里插入图片描述 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/a832d83c091742eda9d9325931a89df4.png) 这里的跟上面的…

【自动化测试】pytest 用例执行中print日志实时输出

author: jwensh date: 20231130 pycharm 中 pytest 用例执行中 print 日志 standout 实时命令行输出 使用场景 在进行 websocket 接口进行测试的时候&#xff0c;希望有一个 case 是一直执行并接受接口返回的数据 def on_message(ws, message):message json.loads(message)…

Liunx配置Tomcat自启动

Liunx配置Tomcat自启动 Tomcat安装配置Tomcat开机启动 Tomcat安装 下载tomcat软件安装包&#xff0c;上传软件包到Liunx服务器。 解压软件包到opt目录下 tar -xvf apache-tomcat-9.0.76.tar.gz -c /opt配置Tomcat开机启动 &#xff08;1&#xff09;修改Tomcat bin目录下的ca…

有”亿“点强,抖音的服务器带宽是如何应对亿人同时刷屏的?

在当今这个“网络横行”的时代&#xff0c;刷短视频已然成为许多人日常生活的一部分。 当我们在刷短视频的时候&#xff0c;尽管家里的网速并不慢&#xff0c;但短视频播放的卡顿却让人难以忍受&#xff0c;有时候真的让人想扔掉手机。 那么&#xff0c;为什么会出现这种情况…

Elk+Filebeat+Kafka实现日志收集

ElkFilebeatKafka实现日志收集(本机nginx) 部署Zookeeper 1.实验组件 #准备3台服务器做Zookeeper集群 20.0.0.10 20.0.0.20 20.0.0.30 2.安装前准备 #关闭防火墙 systemctl stop firewalld systemctl disable firewalld setenforce 0#安装JDK yum install -y java-1.8.0-o…

【LeetCode】每日一题 2023_11_29 无限集中的最小数字(哈希/堆)

文章目录 刷题前唠嗑题目&#xff1a;无限集中的最小数字题目描述代码与解题思路偷看大佬题解 结语 刷题前唠嗑 LeetCode&#xff1f;启动&#xff01;&#xff01;&#xff01; 今天的题目也比较的简单&#xff0c;因为数据量不大&#xff0c;所以什么做法都能过的去 题目&a…

Ruoyi-Vue或者Ruoyi-Cloud登录进去之后的第一个页面如何修改(即如何去掉首页或者如何修改首页)

其实大家如果看过最近的码云上的issues 就能知道这个问题的答案了。 我这里给出一下链接&#xff1a;https://gitee.com/y_project/RuoYi-Vue/issues/I60JIY 开始 第一步&#xff0c;把router/index.js里面关于首页的路由给注释掉或者删除掉&#xff0c;如图&#xff1a; 第…

plt创建指定色系

1、创建不连续色系 import matplotlib.pyplot as plt from matplotlib.colors import ListedColormap# 定义颜色的RGB值 colors [(0.2, 0.4, 0.6), # 蓝色(0.8, 0.1, 0.3), # 红色(0.5, 0.7, 0.2),(0.3,0.5,0.8)] # 绿色# 创建色系 cmap ListedColormap(colors)# 绘制…

化妆品大卖!年轻女孩26天狂赚7000万!

有个年轻的女孩&#xff0c;我们暂且称之为小美。小美经营着一个美丽的小程序商城&#xff0c;里面销售着各种各样的化妆品、日用百货和小家电。这些产品并非什么稀有品牌&#xff0c;但价格比其他地方要优惠一些&#xff0c;更重要的是&#xff0c;购买产品还能赚到钱。 让我们…

微信小程序 - 开发版、体验版、正式版共享本地缓存

问题描述 最近突然发现一个大问题啊&#xff0c;小程序切换版本环境的时候发现数据被污染了&#xff0c;瞬间就怀疑不同环境版本的小程序本地缓存是否共享的&#xff1f;&#xff01; 果然是&#xff01; 解决方案 我们可能马上想到解决方案就是&#xff1a;给每一个环境版本…

人工智能“排头兵”,探访福州多地 AI 智算实践

生成式 AI 在 2023 年再次引爆 IT 技术发展&#xff0c;福建作为数字中国的重要策源地&#xff0c;也是国家数字经济创新发展试验区&#xff0c;在人工智能方面拥有良好的产业基础和人才优势&#xff0c;同时近期出台的《福建省促进人工智能产业发展十条措施》&#xff0c;为福…

【UE】简单的警觉系统

效果 步骤 1. 新建一个空白工程&#xff0c;添加第三人称游戏内容包 2. 打开第三人称角色蓝图“BP_ThirdPersonCharacter” 选中弹簧臂组件&#xff0c;将目标臂长度设置为600&#xff0c;z轴方向的插槽偏移设置为100 3. 将“BP_ThirdPersonCharacter”移入场景&#xff0c;该…

[CustomMessages] section

[CustomMessages] section用来定义自定义的一些{cm:}常量. 一个定义和使用的例子。 [CustomMessages] CreateDesktopIconCreate a &desktop icon[Tasks] Name: desktopicon; Description: "{cm:CreateDesktopIcon}"CustomMessages是支持带参数的&#xff0c;从…

U-GAT-IT 使用指南

U-GAT-IT 使用指南 网络结构优化目标 论文地址&#xff1a;https://arxiv.org/pdf/1907.10830.pdf 项目代码&#xff1a;https://github.com/taki0112/UGATIT U-GAT-IT 和 Pix2Pix 的区别&#xff1a; U-GAT-IT&#xff1a;主要应用于图像风格转换、图像翻译和图像增强等任务…