【Linux】timerfd——定时器

文章目录

    • 前言
    • 认识 timerfd
      • API timerfd
      • API clock
    • 官方示例
    • 简单使用
    • epoll实现

前言

在 Linux 系统编程中,使用 timerfd 实现定时器功能是一种更加可靠、高效、灵活的方式。本文是对 timerfd 的简单使用,不涉及太过深入知识,熟练掌握几个常用 API,实现定时器功能。

认识 timerfd

timerfd 是 Linux 为用户程序提供的一个定时器接口。这个接口基于文件描述符,通过文件描述符的可读事件进行超时通知,它可以用来实现高精度的定时器,以及与 I/O 多路复用机制(如selectpollepoll等)进行配合,实现异步事件驱动的程序。

优点:

  1. 高精度:timerfd使用内核提供的高精度计时器,精度通常可以达到纳秒级别,相对于使用系统时间的方式,误差更小,定时器的精度更高。

  2. 资源占用低:timerfd使用内核提供的异步I/O机制,可以将定时器超时事件通知给应用程序,而不需要应用程序不断地轮询,从而避免了大量的CPU资源占用,提高了系统性能。

  3. 灵活性强:timerfd可以设置不同的定时器超时时间,并且可以同时管理多个定时器,可以根据应用程序的需要进行灵活配置。

  4. 可移植性好:timerfd是Linux内核提供的标准接口,可以在不同的Linux系统上使用,具有很好的可移植性。

API timerfd

常用的 API 接口:

#include <sys/timerfd.h>

int timerfd_create(int clockid, int flags);

int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);

int timerfd_gettime(int fd, struct itimerspec *curr_value);
  • timerfd_create
timerfd_create(CLOCK_REALTIME, 0); // 使用系统实时时间作为计时基准,阻塞模式。

clockid:
	CLOCK_REALTIME:使用系统实时时钟,可以被修改;
	CLOCK_MONOTONIC:使用系统单调时钟,不能被修改;
flags:
	TFD_NONBLOCK:非阻塞模式,read操作不会阻塞;
	TFD_CLOEXEC:创建的文件描述符将在执行exec时关闭。
  • timerfd_settime
timerfd_settime(fd, TFD_TIMER_ABSTIME, &new_value, NULL); 
// 计时器文件描述符fd,初始状态new_value,并且采用绝对时间。(0:相对,1:绝对)

fd:   		 timerfd_create函数创建的定时器文件描述符
flags:		 可以设置为0或者TFD_TIMER_ABSTIME,用于指定定时器的绝对时间或者相对时间
new_value:	 是一个指向itimerspec结构体的指针,用于指定新的定时器参数;
old_value:  是一个指向itimerspec结构体的指针,用于获取旧的定时器参数。
  • timerfd_gettime
// timerfd_gettime是一个用于获取timerfd定时器的当前超时时间的系统调用。
// 它可以用于查询定时器的当前状态,以便在需要时进行调整。

curr_value是指向itimerspec结构体的指针,用于存储当前定时器的超时时间和间隔。

  • itimerspec 结构体定义如下:
struct timespec {
    time_t tv_sec;   // seconds
    long   tv_nsec;  // nanoseconds
};

struct itimerspec {
    struct timespec it_interval;  // Interval for periodic timer
    struct timespec it_value;     // Initial expiration
};

API clock

#include <time.h>
int clock_settime(clockid_t clockid, const struct timespec *tp);
int clock_gettime(clockid_t clockid, struct timespec *tp);
  • clock_settime
// 用于设置系统时钟的时间
// clock_settime函数所设置的时间值会影响到所有基于该时钟的时间函数,例如gettimeofday、clock_gettime等。
// 在Linux系统中,只有特权进程(即具有root权限的进程)才能修改系统时钟。
  • clock_gettime
clock_gettime(CLOCK_MONOTONIC, &start); // 基于系统启动时间的单调递增时钟,获取到的结构体值赋给start

clockid:
	CLOCK_MONOTONIC:提供了一个单调递增的时钟,不受系统时间被改变的影响。
	CLOCK_REALTIME:供了当前的日期和时间,但是受系统时间被改变的影响。

官方示例

官方简单示例:(man手册查看)

#include <sys/timerfd.h>
#include <time.h>
#include <unistd.h>
#include <inttypes.h>      /* Definition of PRIu64 */
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>        /* Definition of uint64_t */

#define handle_error(msg) \
        do { perror(msg); exit(EXIT_FAILURE); } while (0)

static void
print_elapsed_time(void)
{
    static struct timespec start; // 静态,只用于第一次赋值
    struct timespec curr;
    static int first_call = 1;
    int secs, nsecs;

    if (first_call) {
        first_call = 0;
        if (clock_gettime(CLOCK_MONOTONIC, &start) == -1)
            handle_error("clock_gettime");
    }

    if (clock_gettime(CLOCK_MONOTONIC, &curr) == -1)
        handle_error("clock_gettime");

    secs = curr.tv_sec - start.tv_sec;
    nsecs = curr.tv_nsec - start.tv_nsec;
    if (nsecs < 0) {
        secs--;
        nsecs += 1000000000;
    }
    printf("%d.%03d: ", secs, (nsecs + 500000) / 1000000);
}

int
main(int argc, char *argv[])
{
    struct itimerspec new_value;
    int max_exp, fd;
    struct timespec now;
    uint64_t exp, tot_exp;
    ssize_t s;

    if ((argc != 2) && (argc != 4)) {
        fprintf(stderr, "%s init-secs [interval-secs max-exp]\n", // 这里需要输入参数:(第一次到期,间隔,次数)
                argv[0]);
        exit(EXIT_FAILURE);
    }

    if (clock_gettime(CLOCK_REALTIME, &now) == -1)
        handle_error("clock_gettime");

    /* Create a CLOCK_REALTIME absolute timer with initial
        expiration and interval as specified in command line */

    new_value.it_value.tv_sec = now.tv_sec + atoi(argv[1]);
    new_value.it_value.tv_nsec = now.tv_nsec;
    if (argc == 2) {
        new_value.it_interval.tv_sec = 0;
        max_exp = 1;
    } else {
        new_value.it_interval.tv_sec = atoi(argv[2]);
        max_exp = atoi(argv[3]);
    }
    new_value.it_interval.tv_nsec = 0;

    fd = timerfd_create(CLOCK_REALTIME, 0); 
    if (fd == -1)
        handle_error("timerfd_create");

    if (timerfd_settime(fd, TFD_TIMER_ABSTIME, &new_value, NULL) == -1)
        handle_error("timerfd_settime");

    print_elapsed_time();
    printf("timer started\n");

    for (tot_exp = 0; tot_exp < max_exp;) {
        s = read(fd, &exp, sizeof(uint64_t));
        if (s != sizeof(uint64_t))
            handle_error("read");

        tot_exp += exp;
        print_elapsed_time();
        printf("read: %" PRIu64 "; total=%" PRIu64 "\n", exp, tot_exp);
    }

    exit(EXIT_SUCCESS);
}

简单使用

timerfd 的基本使用:

#include <stdio.h>
#include <errno.h> // EAGAIN
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>
#include <pthread.h>
#include <inttypes.h>
#include <sys/time.h>
#include <sys/timerfd.h>

#define handle_error(msg) \
        do { perror(msg); exit(EXIT_FAILURE); } while (0)

int g_exp = 1; // 第四个参数,执行次数
int g_timerfd = -1; // 计时器fd
struct timespec g_current; // 当前时间结构体
bool g_running = true;

void stop_timer(int); // 前置声明

void signal_handler(int signum) {
    stop_timer(g_timerfd);
    close(g_timerfd);
    g_timerfd = -1;
    g_running = false;
    // exit(EXIT_FAILURE);
}

// man timerfd_create
int create_timer() {
    // int fd = timerfd_create(CLOCK_MONOTONIC, 0);
    int fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
    if (fd == -1) 
        handle_error("create_timer failed");
    return fd;
}

void init_timer(int timerfd, time_t secs, long nsecs, time_t interval_secs, long interval_nsecs) {
    struct itimerspec new_value;
    new_value.it_value.tv_sec = secs;
    new_value.it_value.tv_nsec = nsecs;

    new_value.it_interval.tv_sec = interval_secs;
    new_value.it_interval.tv_nsec = interval_nsecs;
    
    if (timerfd_settime(timerfd, 0, &new_value, NULL) == -1)
        handle_error("init_time failed");
}

void stop_timer(int timerfd) {
    struct itimerspec new_value;
    new_value.it_value.tv_sec = 0;
    new_value.it_value.tv_nsec = 0;

    new_value.it_interval.tv_sec = 0;
    new_value.it_interval.tv_nsec = 0;

    if (timerfd_settime(timerfd, 0, &new_value, NULL) == -1) 
        handle_error("stop_timer failed");
}

// 打印间隔时间
static void print_elapsed_time() {
    static struct timespec start;
    static int first_call = 1; 

    if (first_call) {
        first_call = 0;
        if (clock_gettime(CLOCK_MONOTONIC, &start) == -1)
            handle_error("clock_gettime failed");
    }

    if (clock_gettime(CLOCK_MONOTONIC, &g_current) == -1)
        handle_error("clock_gettime failed");

    time_t secs = g_current.tv_sec - start.tv_sec;
    long  nsecs = g_current.tv_nsec - start.tv_nsec;
    if (nsecs < 0) {
        secs -= 1;
        nsecs += 1000000000;
    }
    printf("%ld.%03ld: ", secs, (nsecs + 500000) / 1000000);
}

// 工作线程
void *work_function(void *timerfd) {
    int fd = *(int *)timerfd; 

    uint64_t exp; 
    for (int cnt = 0; cnt < g_exp && g_running; cnt++) {
        int ret = read(fd, &exp, sizeof(uint64_t));
        if (ret == sizeof(uint64_t)) {
            print_elapsed_time();
        } else { // 非阻塞模式,死循环,消耗CPU
            cnt -= 1;
        }
    }
    return NULL;
}

int main(int argc, char *argv[]) {
    // 注册信号
    signal(SIGINT, signal_handler); // kill -l 

    if (argc != 4) {
        fprintf(stderr, "Usage %s init-secs interval-secs max-exp\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    
    g_timerfd = create_timer();
    init_timer(g_timerfd, atoi(argv[1]), 0, atoi(argv[2]), 0);
    g_exp = atoi(argv[3]);
    print_elapsed_time(); // 第一次

    pthread_t tid; 
    pthread_create(&tid, NULL, work_function, &g_timerfd);
    pthread_join(tid, NULL);
    return 0;   
}

上例程序是非阻塞的timerfd,极大占用CPU资源。

在这里插入图片描述


epoll实现

timerfd、eventfd、signalfd配合epoll使用,可以构造出一个零轮询的程序,但程序没有处理的事件时,程序是被阻塞的。epoll_wait阻塞监听事件。

  • eventfd
#include <sys/eventfd.h>
int eventfd(unsigned int initval, int flags);
// 其中initval参数是eventfd的初始值,通常设置为0即可,表示初始时没有事件发生。
// flags参数是eventfd的标志位,通常设置为0即可,表示没有特殊要求。
// 函数返回值是eventfd的文件描述符,可以用于后续的读写操作。

eventfd是Linux系统调用中的一种文件描述符类型,它可以用于在进程间传递事件通知。当一个进程调用eventfd创建一个事件文件描述符时,它会得到一个文件描述符,可以使用该文件描述符进行读写操作。当另一个进程写入数据到该文件描述符时,它会唤醒正在等待读取该文件描述符的进程。这样,可以实现进程间的事件通知,而无需使用显式的信号量或者管道等机制。

eventfd的使用非常简单,只需要调用eventfd函数创建一个事件文件描述符,然后使用read和write函数进行读写操作即可。eventfd还支持select、poll、epoll等多路复用机制,可以方便地将其集成到事件驱动的程序中。


代码如下:

  • timerfd设置为非阻塞状态:
fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
  • 创建eventfd
g_eventfd = eventfd(0, 0);
  • 注册epoll事件监听
int epollfd = epoll_create(2);
if (epollfd == -1)
    handle_error("epoll_create failed");

struct epoll_event ev_timer;
ev_timer.events = EPOLLIN;
ev_timer.data.fd = fd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev_timer);

struct epoll_event ev_event;
ev_event.events = EPOLLIN;
ev_event.data.fd = g_eventfd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, g_eventfd, &ev_event);

const int max_events = 2;
struct epoll_event events[max_events];

详见代码:

#include <stdio.h>
#include <errno.h> // EAGAIN
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>
#include <pthread.h>
#include <inttypes.h>
#include <sys/time.h>
#include <sys/epoll.h> 
#include <sys/eventfd.h>
#include <sys/timerfd.h>

#define handle_error(msg) \
        do { perror(msg); exit(EXIT_FAILURE); } while (0)

int g_exp = 1; // 第四个参数,执行次数
int g_timerfd = -1; // 计时器fd
struct timespec g_current; // 当前时间结构体
bool g_running = true;
int g_eventfd; // a file descriptor for event notification

void stop_timer(int); // 前置声明

void signal_handler(int signum) {
    stop_timer(g_timerfd);

    g_timerfd = -1;
    g_running = false;
    
    uint64_t g_eventfd_val = 10086; // 64位无符号!!!;不要用 -1
    write(g_eventfd, &g_eventfd_val, sizeof(uint64_t));
}

// man timerfd_create
int create_timer() {
    // int fd = timerfd_create(CLOCK_MONOTONIC, 0);
    int fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
    if (fd == -1) 
        handle_error("create_timer failed");
    return fd;
}

void init_timer(int timerfd, time_t secs, long nsecs, time_t interval_secs, long interval_nsecs) {
    struct itimerspec new_value;
    new_value.it_value.tv_sec = secs;
    new_value.it_value.tv_nsec = nsecs;

    new_value.it_interval.tv_sec = interval_secs;
    new_value.it_interval.tv_nsec = interval_nsecs;
    
    if (timerfd_settime(timerfd, 0, &new_value, NULL) == -1)
        handle_error("init_time failed");
}

void stop_timer(int timerfd) {
    struct itimerspec new_value;
    new_value.it_value.tv_sec = 0;
    new_value.it_value.tv_nsec = 0;

    new_value.it_interval.tv_sec = 0;
    new_value.it_interval.tv_nsec = 0;

    if (timerfd_settime(timerfd, 0, &new_value, NULL) == -1) 
        handle_error("stop_timer failed");
}

// 打印间隔时间
static void print_elapsed_time() {
    static struct timespec start;
    static int first_call = 1; 

    if (first_call) {
        first_call = 0;
        if (clock_gettime(CLOCK_MONOTONIC, &start) == -1)
            handle_error("clock_gettime failed");
    }

    if (clock_gettime(CLOCK_MONOTONIC, &g_current) == -1)
        handle_error("clock_gettime failed");

    time_t secs = g_current.tv_sec - start.tv_sec;
    long  nsecs = g_current.tv_nsec - start.tv_nsec;
    if (nsecs < 0) {
        secs -= 1;
        nsecs += 1000000000;
    }
    printf("%ld.%03ld\n", secs, (nsecs + 500000) / 1000000);
}

// 工作线程
void *work_function(void *timerfd) {
    int fd = *(int *)timerfd; 

    int epollfd = epoll_create(2);
    if (epollfd == -1)
        handle_error("epoll_create failed");
    
    struct epoll_event ev_timer;
    ev_timer.events = EPOLLIN;
    ev_timer.data.fd = fd;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev_timer);

    struct epoll_event ev_event;
    ev_event.events = EPOLLIN;
    ev_event.data.fd = g_eventfd;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, g_eventfd, &ev_event);

    const int max_events = 2;
    struct epoll_event events[max_events];

    uint64_t exp; 
    for (int cnt = 0; cnt < g_exp && g_running; cnt++) {
        int nfd = epoll_wait(epollfd, events, max_events, -1);

        if (nfd > 0) {
            for (int i = 0; i < nfd; i++) {
                if (events[i].data.fd == fd) {
                    int ret = read(fd, &exp, sizeof(uint64_t));
                    if (ret == sizeof(uint64_t)) {
                        print_elapsed_time();
                    }
                } else if (events[i].data.fd == g_eventfd) {
                    int ret = read(g_eventfd, &exp, sizeof(uint64_t));
                    if (ret == sizeof(uint64_t)) {
                        if (exp == 10086) { // 64位无符号!!!;不要用 -1
                            close(fd);
                            epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
                            close(g_eventfd);
                            epoll_ctl(epollfd, EPOLL_CTL_DEL, g_eventfd, NULL);
                            return NULL;
                        }
                    }
                }
            }
        }
    }
    close(fd);
    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
    close(g_eventfd);
    epoll_ctl(epollfd, EPOLL_CTL_DEL, g_eventfd, NULL);
    return NULL;
}

int main(int argc, char *argv[]) {
    // 注册信号
    // signal(SIGINT, signal_handler); // kill -l 
    signal(SIGUSR1, signal_handler); // kill -SIGUSR1 <pid>: 使用 SIGUSR1 和 SIGUSR2 信号来进行信号传递。这两个信号是专门为用户定义的信号,可以用于进程间通信或者在同一进程的不同线程之间进行通信。

    if (argc != 4) {
        fprintf(stderr, "Usage %s init-secs interval-secs max-exp\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    
    g_eventfd = eventfd(0, 0);
    if (g_eventfd == -1) 
        handle_error("eventfd failed");

    g_timerfd = create_timer();
    init_timer(g_timerfd, atoi(argv[1]), 0, atoi(argv[2]), 0);
    g_exp = atoi(argv[3]);
    print_elapsed_time(); // 第一次

    pthread_t tid; 
    pthread_create(&tid, NULL, work_function, &g_timerfd);
    pthread_join(tid, NULL);
    return 0;   
}

运行示例:(1s第一次到期,每个3s定时一次,总共10次,kill杀死信号后结束程序)

在这里插入图片描述

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

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

相关文章

DOCker安装(一)

DOCker的安装 1、简介 Docker使用客户端-服务器(C/S)架构模式&#xff0c;使用远程API来管理和创建Docker容器。 Docker容器通过Docker镜像来创建。 容器之间互不干扰 容器与镜像的关系类似于面向对象编程中的对象与类。 对象->容器 镜像->类 通过镜像来创建容器 …

从Web2.0走向Web3.0还有多远?

Web2.0时代给互联网带来了巨大的变革&#xff0c;让用户成为内容的创造者和共享者。然而&#xff0c;随着技术的不断发展和创新&#xff0c;我们正在逐渐迈向Web3.0时代&#xff0c;这将是一个更加去中心化、透明和安全的数字世界。那么&#xff0c;从Web2.0走向Web3.0还有多远…

PHP基础知识解析:探索PHP编程的核心概念和技巧

目录 PHP简介 什么是PHP 网站基本概念 网站 静态网站特点 动态网站特点 服务器概念 IP 域名 DNS 端口 PHP基础知识 PHP语法 PHP注释 PHP语句分隔符 变量 变量基本概念 变量的使用 命名的命名规则&#xff1a; 预定义变量&#xff1a; 可变变量 变量传值 …

Pytest中断言的重要性

目录 前言 pytest断言 增加断言详细信息 异常断言 .type .value .traceback pytest常用断言 前言 在pytest中&#xff0c;断言是非常重要的一部分。断言可以帮助我们验证代码的正确性&#xff0c;检查函数返回的值是否符合要求&#xff0c;以及判断程序中预期行为是否发生。如…

Idea Mybatis插件:提高CRUD效率

mybatis-sql-viewer插件主要提供能力&#xff1a;将mybatis xml转成真实SQL语句、参数mock、SQL规范检查、SQL索引检查、SQL运行、SQL压测及Mybatis SQL语句扫描。 1. 简介 虽然写了很久的CRUD&#xff0c;但是依旧觉得写好CRUD是一件非常难且麻烦的事情&#xff0c;以下的情…

南京阿里云代理商:阿里云服务器的可扩展性和弹性如何?是否支持按需付费?

南京阿里云代理商&#xff1a;阿里云服务器的可扩展性和弹性如何&#xff1f;是否支持按需付费&#xff1f;   一、阿里云服务器的可扩展性   阿里云作为业界知名的云服务提供商&#xff0c;其服务器具有极强的可扩展性。可扩展性主要体现在以下几方面&#xff1a;   1. …

C++哈希表

目录 介绍哈希概念哈希冲突哈希函数解决哈希冲突 闭散列介绍线性探测二次探测负载因子 实现哈希表结构哈希函数元素查找插入元素删除元素 开散列介绍实现哈希表结构元素查找插入元素删除元素析构函数 介绍 哈希概念 了解过搜索二叉树与红黑树后&#xff0c;它们的结构特点主要…

测试用例excel转word(Office word篇)

场景 我们在项目中&#xff0c;默认情况下是用我们的Excel用例模版输出测试用例。但是有的项目中&#xff0c;会要求在Word版本的测试计划或者测试报告中&#xff0c;写明测试用例。而我们的测试用例&#xff0c;有的项目有上千条&#xff0c;这个时候如果从Excel往Word中复制…

Cortext-M3系统:异常(3)

1、异常 异常响应系统是再M3内核水平上的&#xff0c;支持众多的系统异常和外部中断。1-15为系统异常&#xff0c;大于16为外部中断。除了个别异常的优先级被定死外&#xff0c;其它异常的优先级都是可编程的。优先级数值越小&#xff0c;优先级越高。CM3支持中断嵌套&#xff…

MyBatis面试题

什么是 MyBatis&#xff1f; MyBatis 是一个半 ORM&#xff08;对象关系映射&#xff09;框架&#xff0c;它内部封装了 JDBC&#xff0c;开发时只需要关注 SQL 语句本身&#xff0c;不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。程序员直接编写原生…

机器学习——识别足球和橄榄球

一、选题的背景 橄榄球起源于足球&#xff0c;二者即相似又有所区别。计算机技术发展至今&#xff0c;AI技术也有了极大的进步&#xff0c;通过机器学习不断的训练&#xff0c;AI对于足球和橄榄球的识别能力可以帮助人们对足球和橄榄球的分辨。机器学习是一种智能技术&#xff…

详解Http的Content-Type

目录 1.概述 2.常用类型 2.1.application/x-www-form-urllencoded 2.2.application/json 3.Spring MVC支持的编码 3.1.实验 3.2.适配器 3.3.自定义适配器 1.概述 HTTP&#xff08;HyperText Transfer Protocol&#xff09;&#xff0c;超文本传输协议。超文本&#xf…

GEE:将地形山体阴影和类别概率信息结合起来,绘制概率山体阴影(Probability Hillshade)图

作者:CSDN @ _养乐多_ 本文将介绍使用哨兵数据将地形山体阴影和类别概率信息结合起来,绘制概率山体阴影图的代码。 “Probability Hillshade”(概率山体阴影)是指使用Dynamic World数据集中最可能的类别概率信息创建的一种可视化效果。它结合了地形山体阴影和类别概率信息…

【pytorch】新的windows电脑从头搭建pytorch深度学习环境(完整版+附安装包)

文章目录 新的windows电脑搭建pytorch深度学习环境电脑环境的配置显卡驱动cudacudnn pytorch开发软件的安装minicondavscode pytorch环境的安装conda安装python环境安装pytorch和torchvision 附录1&#xff1a;部分torch、torchvision、torchaudio版本对应关系附录2&#xff1a…

iOS App 上架流程图文教学

在上架App 之前必须先准备好开发者帐号&#xff0c;但申请开发者帐号因法兰克早在之前已经申请好了&#xff0c;故就跳过此步骤&#xff0c;直接从产生凭证到上传App开始讲起。首先&#xff0c;要将自己辛苦写好的App 送审的话&#xff0c;则要依序做完下列几件事情即可。 在开…

常见面试题之框架篇

1.Spring框架中的单例bean是线程安全的吗&#xff1f; 不是线程安全的&#xff0c;是这样的。 当多用户同时请求一个服务时&#xff0c;容器会给每一个请求分配一个线程&#xff0c;这是多个线程会并发执行该请求对应的业务逻辑&#xff08;成员方法&#xff09;&#xff0c;…

TensorFlow Core—基本分类:对服装图像进行分类

现在人工智能很火的&#xff0c;看到了这篇文章&#xff0c;给自己普及一下基础知识&#xff0c;也分享给大家&#xff0c;希望对大家有用。 本指南将训练一个神经网络模型&#xff0c;对运动鞋和衬衫等服装图像进行分类。即使您不理解所有细节也没关系&#xff1b;这只是对完…

3ds Max - Pivot Painter Tool

很久之前的笔记&#xff0c;整理归档&#xff1b; Pivot Painter Tool是3dsMax中的插件&#xff0c;主要是辅助将Mesh中每个Element生成自己的Pivot Position&#xff0c;方便如使用World Position Offset对每个Element进行精确控制&#xff0c;导入使用Pivot Painter Tool工具…

二进制搭建 Kubernetes v1.20

k8s集群master01&#xff1a;192.168.179.25 kube-apiserver kube-controller-manager kube-scheduler etcd k8s集群master02&#xff1a;192.168.179.26 k8s集群node01&#xff1a;192.168.179.23 kubelet kube-proxy docker k8s集群node02&#xff1a;192.168.179.22 …

统信UOS系统开发笔记(六):提取在线安装软件后,提取其安装包,部署目标机使用离线软件包方式安装软件

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/131348876 红胖子(红模仿)的博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软…