2024年2月

如今,大模型层出不穷,这为自然语言处理、计算机视觉、语音识别和其他领域的人工智能任务带来了重大的突破和进展。大模型通常指那些参数量庞大、层数深、拥有巨大的计算能力和数据训练集的模型。

但不能不承认的是,普通人使用大模型还是有一定门槛的,首先大模型通常需要大量的计算资源才能进行训练和推理。这包括高性能的图形处理单元(GPU)或者专用的张量处理单元(TPU),以及大内存和高速存储器。说白了,本地没N卡,就断了玩大模型的念想吧。

其次,大模型的性能往往受到模型调优和微调的影响。这需要对模型的超参数进行调整和优化,以适应特定任务或数据集。对大模型的调优需要一定的经验和专业知识,包括对深度学习原理和技术的理解。

那么,如果不具备相关专业知识,也没有专业的设备,同时也想开发属于自己的基于AI大模型的应用怎么办?本次我们使用在线的云雀大模型来打造属于自己的AI应用。

构建线上AI应用

首先访问扣子应用的官网:

https://www.coze.cn/home

注册成功之后,我们需要一个创意,也就是说我们到底想要做一个什么应用,这个应用的功能是什么,当然,关于创意AI是帮不了你的,需要自己想,比如笔者的代码水平令人不敢恭维,平时在CodeReView时,经常被同事嘲笑,没办法,有的人就是没有代码洁癖,为了避免此种情况经常发生,想要打造一款AI机器人能够在代码提交之前帮忙审核代码,检查语法的错误并给出修改意见和性能层面优化的方案。

此时点击创建Bot:

如上图所示,这里输入应用的名称和描述,至于应用图标,可以让AI生成一个。

工作流 WorkFlow

工作流指的是支持通过可视化的方式,对插件、大语言模型、代码块等功能进行组合,从而实现复杂、稳定的业务流程编排。

我们的工作流非常简单,第一步,用户输入代码,第二步,大模型检查代码,第三步,大模型返回修改意见。

点击创建工作流,输入的参数变量为code,数据类型可以选择四种,即字符串、整形、布尔和数字,这里代码显然选择字符串。

接着接入大模型,引导词这样填写:

你是一位经验丰富的程序员,擅长Python,HTML,CSS,Java,Golang等语言,请检查{{input}}这段代码的语法,给出修改的建议,并且给出代码性能优化的多种修改方案。

通过引导词来让大模型针对输入的代码进行检测。

最后返回大模型输入的修改意见。

如图所示:

随后可以运行整个工作流进行测试,大模型返回:

{  
  "output": "可优化项目与建议:\n- 建议使用 `try-except` 结构来捕获所有类型的异常,然后进行统一处理。\n- 建议添加代码注释,增强代码的可读性。\n\n优化后的代码如下所示:\n```python\ndef make_tran():\n    \"\"\"\n    该函数用于将 video.srt 文件中的英文翻译成中文,并将翻译结果写入 two.srt 文件\n\n    Returns:\n        str: \"翻译完毕\",表示翻译过程已完成\n    \"\"\"\n    # 从预训练模型中加载 tokenizer 和 model\n    tokenizer = AutoTokenizer.from_pretrained(\"Helsinki-NLP/opus-mt-en-zh\")\n    model = AutoModelForSeq2SeqLM.from_pretrained(\"Helsinki-NLP/opus-mt-en-zh\")\n    # 打开 video.srt 文件并读取其中的内容\n    with open(\"./video.srt\", 'r', encoding=\"utf-8\") as file:\n        gweight_data = file.read()\n    # 将读取到的内容按换行符分割成多个段落,并存储在 result 列表中\n    result = gweight_data.split(\"\\n\\n\")\n    # 如果 two.srt 文件存在,则将其删除\n    if os.path.exists(\"./two.srt\"):\n        os.remove(\"./two.srt\")\n    # 遍历 result 列表中的每个段落\n    for res in result:\n        # 将每个段落按换行符分割成多个句子,并存储在 line_srt 列表中\n        line_srt = res.split(\"\\n\")\n        # 尝试对每个句子进行翻译\n        try:\n            # 使用 tokenizer 对句子进行预处理,以便模型可以处理它们\n            tokenized_text = tokenizer.prepare_seq2seq_batch([line_srt[2]], return_tensors='pt')\n            # 使用模型进行翻译\n            translation = model.generate(**tokenized_text)\n            # 使用 tokenizer 将翻译结果解码为文本\n            translated_text = tokenizer.batch_decode(translation, skip_special_tokens=False)[0]\n            # 移除翻译结果中的填充字符和结束符号,并去除首尾的空格\n            translated_text = translated_text.replace(\"<pad>\", \"\").replace(\"</s>\", \"\").strip()\n            # 打印翻译结果\n            print(translated_text)\n            # 将翻译结果写入 two.srt 文件\n            with open(\"./two.srt\", \"a\", encoding=\"utf-8\") as f:\n                f.write(f\"{line_srt[0]}\\n{line_srt[1]}\\n{line_srt[2]}\\n{translated_text}\\n\\n\")\n        # 如果在翻译过程中发生任何异常,则打印异常信息,并跳过当前句子\n        except Exception as e:\n            print(str(e))\n    # 返回 \"翻译完毕\",表示翻译过程已完成\n    return \"翻译完毕\"\n```"  
}

如此,就完成了一个代码检查和优化的工作流,说白了,就是给用户一个没有token限制并且无限次使用的大模型,并且跳过prompt环节,直接简单粗暴返回垂直内容的解决方案。

发布应用

构建好应用之后,我们可以在其他平台发布,让更多人使用该应用,这里以飞书为例子,飞书是一站式协同办公平台,为企业提供各种数字化办公解决方案,大部分公司都在使用。

随后在公司群里就可以直接调用自己的应用了:

结语

尽管使用大模型可能具有一些挑战,但随着技术的进步和资源的可用性,大模型的门槛正在逐渐降低。这为更多的普通人、无编程背景的爱好者提供了利用大模型来解决对于个人垂直领域相对复杂任务的机会。

背景介绍

最近比较悠闲,于是没事研究了一下某东的h5st代码,2024年新鲜出炉的前端加密代码;

最大的惊喜并不是算法的复杂,在逆向破解代码的过程中,对js加密混淆有了新的认识;

于是心血来潮,回到这里,写一份研究总结,供技术交流分享。


代码分析

拿到的代码是h5st的4.3版本

使用开发者工具,显示的风格是这样的:

直接搜索关键字"h5st",整份文件里面就出现了2次,所以这里很容易找到出口。(PS:如果还有4.4版本,建议把这个关键字也加密,加大破解难度)

注意看10787行这个switch语法,在研究的过程中,这个语法出现概率非常高,它的作用就是把串行的代码用switch改变了代码的书写顺序,例如:

var a = 1;var b = a+1;var alert(b)j

加密之后就变成了:

var config = '3|1|2';var lines = config.split('|');var i = 0;switch(lines[i++]){case 1:var b = a+1;break;case 2:returnalert(b);case 3:var a = 1;break;
}

还有就是变量的混淆,总体上分为两类:

//原始代码
alert('hi');//加密后
functiona1(t,r){returnt(r)
}
functionb1(t,r){return ['','hi'][t-r];
}
a1(alert,b1(
699,698));

整个研究过程中,这些加密顶多是费点时间,要说最痛苦的,莫过于这一段:

    functionHv(t, r, n, e, o, i, u) {try{var a =t[i](u)
, c
=a.value
}
catch(t) {return voidn(t)
}
a.done
?r(c) : Kv.resolve(c).then(e, o)
}

这种不是串行的代码,类似无限递归的函数,理解起来最为费劲;

所以算法里面,最让人头痛的也是第8个关键参数,因为很难找到加密的原始字符串来源是什么。

庆幸的是,如果不是要求很高,那么可以按照它的格式,自己撰写一个,不需要去研究具体来源;

PS:如果想研究每个字段的计算过程,那么参考搜索的代码是"switch (t[kn(741, 0, p)] = t.next) "


算法总结

位置 字段名 说明 值格式 说明
1 time 时间字符串 20240131103354895 当前时间
2 fingerPrint 指纹 hnmy1t6fytuu1kt7 自定义算法,就是随机字符串删删减减,固定字符池:kl9i1uct6d
3 appId 应用编号 95cb4 业务页面的appId
4 token 令牌 tk02wad0d1bee41lMiszWWgySTRKO.. 下面这个对象的值.join('')

{
"magic": "tk",
"version": "02",
"platform": "w",
'adler32': "其它参数的校验",
"expires": "41",
"producer": "l",
"expr": "类似3+3+3+2x2的base64编码,生成sign会用到"
"cipher": "HmacSHA256结果,跟fp有关"
}

5 sgin 签名 c40d194fb4...

对请求参数进行HmacSHA256加密

加密使用的key生成算法

最初原始字符串:token + fp + timestamp + "22" + appid + "Z=<J_2";

从token中获取签名算法字符串,类似:3+3+3+2x2

数字表示用第一套加密,运算符,表示是拼接还是多种加密

6 version 算法版本 4.3 固定值
7 timestamp 毫秒时间戳 1706668434895 当前时间
8 sent 环境数据 bb7ef745bba685...

AES-128-CBC加密

原始字符串参考:

{
"sua": "Macintosh; Intel Mac OS X 10_15_7",
"pp": {},
"extend": {"wd": 0, "l": 0, "ls": 5, "wk": 0, "bu1": "0.1.9", "bu2": -1, "bu3": 43, "bu4": 0},
"random": "27_CACK7qU5",
"v": "h5_file_v4.3.3",
"fp": "xxx",
"bu1": "0.2.0"
}


测试过程

好不容易研究完准备验收

发现jd的所有接口都不验证这个参数


所以研究就到此为止了~

前言

Linux POSIX IPC的可移植性是不如System V IPC的,但是我们只用Linux,并且内核版本高于2.6.6的话就不存在该问题了。也因为POSIX IPC出现的比较晚,借鉴了systemV IPC的长处,规避其短处,使得POSIX IPC的接口更易用。进程间通信的手段很多,除了消息队列、信号量、共享内存,还有信号、socket、管道,普通的管道需要祖先进程有联系,具名管道可以应用于无关联的进程。

后文记录的内容都是POSIX IPC的使用。

访问标识

IPC标识符的操作行为都模范了文件描述符,可以像操作文件一样打开标识符。内核会维护该标识的引用计数,删除标识符也就是删除了名字,等引用计数为0时才会真正的销毁。这些标识符会被放在
/dev/shm
目录下。

  • 默认创建消息队列在该目录下看不到,需要我们将消息队列的目录挂载到文件系统中,然后再使用创建函数来创建mq
mkdir /dev/mq
mount -t mqueue none /dev/mq
  • 为了可移植性,给标识符起名以斜线开头后跟非斜线字符的形式,如
    /mysem

消息队列

创建

#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <mqueue.h>

mqd_t mq_open(const char *name, int oflag); // 打开

// 创建
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);

// 成功返回fd,失败返回(mqd_t)-1并设置errno

oflag:

  • O_RDONLY:只接收、O_WRONLY:只发送、O_RDWR:接收和发送
  • O_CLOEXEC:给fd设置close-on-exec
  • O_CREAT:若不存在则创建,存在则直接使用。同时使用
    O_CREATE | O_EXCL
    ,如果已经存在该文件返回errno EEXIST。设置O_CREAT则必须设置fd的权限,即mode
    • S_IRUSER、S_IWUSR、S_IRGRP、S_IWGRP、S_IROTH、SIWOTH
  • O_NONBLOCK:mq_receive和mq_send使用fd默认是阻塞的,该标志设置fd为非阻塞,无数据可接收或可发送时返回 errno EAGAIN

attr:

struct mq_attr {
   long mq_flags;       /* Flags: 0 or O_NONBLOCK */
   long mq_maxmsg;      /* Max. # of messages on queue */
   long mq_msgsize;     /* Max. message size (bytes) */
   long mq_curmsgs;     /* # of messages currently in queue */
};

mq_maxmsg和mq_msgsize在创建时就确定好,创建好后无法再进行调整。只能调制mq_flags设置是否为阻塞

关闭

  1. 接口关闭
#include <mqueue.h>

// 关闭mq,引用计数-1,即使全部使用mq_close关闭,消息队列fd仍然存在,需要使用unlink销毁
int mq_close(mqd_t mqdes);

// 删除,直到引用计数为0才真正删除
int mq_unlink(const char *name);

// 成功返回0,失败返回-1并设置errno
  1. 与普通文件描述符一样,也可以到目录下
    rm
    删除
  2. fork会继承fd,内核实现中消息队列的fd带有O_CLOEXEC,所以当子进程调用exec函数时会自动关闭消息队列

收发消息

#include <mqueue.h>
// 发送消息
int mq_send(mqd_t mqdes, const char msg_ptr[.msg_len],
            size_t msg_len, unsigned int msg_prio);
// msg_len:长度为0~mq_msgsize, 长度超过mq_msgsize返回EMSGSIZE
// msg_prio:消息优先级,最大为MQ_PRIO_MAX,不需要优先级设置为0


// 接收消息,接收优先级最高的消息中最先到达的
ssize_t mq_receive(mqd_t mqdes, char msg_ptr[.msg_len],
                   size_t msg_len, unsigned int *msg_prio);
// msg_len:>=mq_msgsize,可以通过mq_getattr()获取
// msg_prio:NULL表示不关心优先级,非NULL系统将取到的消息体的优先级复制到msg_prio
  • 如果mq已满,mq_send阻塞。如果设置了O_NONBLOCK标志,立即返回EAGIN。同样,如果mq为空,mq_receive阻塞,如果设置了O_NONBLOCK标志,立即返回EAGIN

mq_notify:

// 异步消息通知,消息到来时可以通知进程。该函数用于进程注册或注销消息通知,给sevp传递NULL
int mq_notify(mqd_t mqdes, const struct sigevent *sevp);
  • 同一时间只能有一个进程注册,多个进程注册后面的进程会收到EBUSY错误。
    • 只有注册到空消息队列时,消息到来才会通知进程。如果队列不为空,则注册后要等下次消息队列为空再接收到的消息会给进程发送通知。
    • 通知完成后就会删除进程的注册。
    • 如果先有进程阻塞在mq_receive,那么消息到来不会通知注册的进程,进程状态依然是注册。

const struct sigevent *sevp
的结构如下:

#include <signal.h>

union sigval {            /* Data passed with notification */
   int     sival_int;    /* Integer value */
   void   *sival_ptr;    /* Pointer value */
};

struct sigevent {
   int    sigev_notify;  /* Notification method */
   int    sigev_signo;   /* Notification signal */
   union sigval sigev_value;
                         /* Data passed with notification */
   void (*sigev_notify_function) (union sigval);
                         /* Function used for thread
                            notification (SIGEV_THREAD) */
   void  *sigev_notify_attributes;
                         /* Attributes for notification thread
                            (SIGEV_THREAD) */
   pid_t  sigev_notify_thread_id; // 用在POSIX timers,man timer_create(2)
                         /* ID of thread to signal
                            (SIGEV_THREAD_ID); Linux-specific */
};

sigev_notify可以设置为:

  • SIGEV_NONE:消息到达时不做任何事
  • SIGEV_SIGNAL:采用发送信号的方式通知进程
  • SIGEV_THREAD:创建一个线程,执行segev_notify_function函数

同时因为posix 消息队列标识符有文件描述符的属性,那么在linux下I/O多路复用是更好的选择,下面demo使用epoll监听队列消息

demo

客户端给mq发送消息。server端分别使用
SIGEV_SIGNAL

SIGEV_THREAD
、epoll模式来监听消息队列到来的消息。
先mount消息队列的目录,方便使用文件接口查看

 mount -t mqueue none /dev/mq

客户端:

#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#define OFLAG (O_CREAT | O_EXCL | O_WRONLY)
#define PERM (S_IRUSR | S_IWUSR)

int main(int argc, char *argv[]) {
  if (argc != 2) {
    printf("usage: %s /mqname\n", argv[0]);
    return 1;
  }
  const char *mqname = argv[1];
  mqd_t mq = mq_open(mqname, OFLAG, PERM, NULL);
  struct mq_attr attr;
  mq_getattr(mq, &attr);
  char *buf = (char *)malloc(attr.mq_msgsize);

  while ((fgets(buf, attr.mq_msgsize, stdin) != NULL) && (buf[0] != '\n')) {
    mq_send(mq, buf, attr.mq_msgsize, 0);
  };
  close(mq);
  return 0;
}

信号处理server:

#define _DEFAULT_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <mqueue.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
  if (argc != 2) {
    printf("usage: %s /mqname", argv[0]);
    return 1;
  }
  mqd_t mq;             // 声明mq标识符
  struct mq_attr attr;  // 声明消息属性
  if ((mq = mq_open(argv[1], O_RDONLY | O_NONBLOCK)) == -1) {
    printf("open mq failure\n");
    return 1;
  }
      // 信号处理
  sigset_t mask;
  struct sigevent sigev;
  int sig;
  int num;
  mq_getattr(mq, &attr);
  char *buf = (char *)malloc(attr.mq_msgsize); // 分配消息的缓存空间
  // 设置信号集
  sigemptyset(&mask);
  sigaddset(&mask, SIGUSR1);
  sigprocmask(SIG_BLOCK, &mask, NULL);

  sigev.sigev_notify = SIGEV_SIGNAL;  // 使用信号notify
  sigev.sigev_signo = SIGUSR1;        // 使用信号SIGUSR1
  mq_notify(mq, &sigev);              // 注册notify

  for (;;) {
    sigwait(&mask, &sig);  // 等待信号
    if (sig == SIGUSR1) {
      mq_notify(mq, &sigev); // 再次注册notify
      while ((num = mq_receive(mq, buf, attr.mq_msgsize, NULL)) >= 0) {
        fprintf(stderr, "receive %d bytes, content: %s", num, buf);
      }
    }
  }
  close(mq);
  return 0;
}
// ------------------
root@yielde:~/workspace/code-container/cpp/blog_demo# ./client /mq_signal
hello signal
hello signal 1

root@yielde:~/workspace/code-container/cpp/blog_demo# ./server /mq_signal
receive 8192 bytes, content: hello signal
receive 8192 bytes, content: hello signal 1

线程处理server:

static void notify_function(union sigval sv);

// 线程处理
static void setup_notify(mqd_t *mqp) {
  struct sigevent sig_ev;              // 定义sigevent
  sig_ev.sigev_notify = SIGEV_THREAD;  // 通知到达,启用线程处理
  sig_ev.sigev_notify_function = notify_function;  // 处理函数
  sig_ev.sigev_notify_attributes = NULL;           // 线程属性设置为NULL
  sig_ev.sigev_value.sival_ptr = mqp;
  mq_notify(*mqp, &sig_ev);
}

static void notify_function(union sigval sv) {
  mqd_t *mqp = (mqd_t *)sv.sival_ptr;
  struct mq_attr attr;
  mq_getattr(*mqp, &attr);
  int num = 0;
  char *buf = (char *)malloc(attr.mq_msgsize);  // 保证buf足够存放消息
  setup_notify(mqp);
  while ((num = mq_receive(*mqp, buf, attr.mq_msgsize, NULL)) >= 0) {
    fprintf(stderr, "receive %d bytes, content: %s", num, buf);
  }
}

int main(int argc, char *argv[]) {
  if (argc != 2) {
    printf("usage: %s /mqname", argv[0]);
    return 1;
  }
  mqd_t mq;             // 声明mq标识符
  struct mq_attr attr;  // 声明消息属性
  if ((mq = mq_open(argv[1], O_RDONLY | O_NONBLOCK)) == -1) {
    printf("open mq failure\n");
    return 1;
  }

  // 通过线程处理
  setup_notify(&mq);
  for (;;) {
    pause();
  }
  close(mq);
  return 0;
}

// ----------------------
root@yielde:~/workspace/code-container/cpp/blog_demo# ./server /mq_thread
receive 8192 bytes, content: hello thread
receive 8192 bytes, content: hello thread 1

root@yielde:~/workspace/code-container/cpp/blog_demo# ./client /mq_thread
hello thread
hello thread 1

epoll处理server:epoll的使用请看
I/O多路复用与socket - 佟晖 - 博客园

void add_epoll(int epollfd, int fd) {
  struct epoll_event events;
  events.data.fd = fd;
  events.events = EPOLLIN | EPOLLET;
  epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &events);
}

int main(int argc, char *argv[]) {
  if (argc != 2) {
    printf("usage: %s /mqname", argv[0]);
    return 1;
  }
  mqd_t mq;             // 声明mq标识符
  struct mq_attr attr;  // 声明消息属性
  if ((mq = mq_open(argv[1], O_RDONLY | O_NONBLOCK)) == -1) {
    printf("open mq failure\n");
    return 1;
  }
  // epoll 处理
  struct epoll_event events[10];
  int epollfd = epoll_create(2);
  add_epoll(epollfd, mq);
  mq_getattr(mq, &attr);
  char *buf = (char *)malloc(attr.mq_msgsize);
  while (1) {
    printf("epoll waiting message\n");
    int ret = epoll_wait(epollfd, events, 10, -1);
    if (ret > 0) {
      int num;
      for (int i = 0; i < ret; ++i) {
        int fd = events[i].data.fd;
        if ((fd == mq) && (events[i].events & EPOLLIN)) {
          while ((num = mq_receive(fd, buf, attr.mq_msgsize, 0)) >= 0) {
            printf("receive %d bytes, content: %s", num, buf);
          }
        }
      }
    } else if (ret < 0) {
      printf("events error: %d\n", errno);
      break;
    }
  }

  close(epollfd);
  close(mq);
  return 0;
}
// --------------------
root@yielde:~/workspace/code-container/cpp/blog_demo# ./client /mq_epoll
hello epoll
hello epoll 1

root@yielde:~/workspace/code-container/cpp/blog_demo# ./server /mq_epoll
receive 8192 bytes, content: hello epoll
receive 8192 bytes, content: hello epoll 1

信号量

信号量可以同步进程或线程,协助多个进程或线程之间访问共享资源。信号量分为有名信号量和无名信号量。

  • 有名信号量:有文件标识符,无关进程可以直接打开使用。
  • 无名信号量:没有文件标识符,无法通过open操作打开使用,多用于线程同步

有名信号量API

#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <semaphore.h>

// 打开sem
sem_t *sem_open(const char *name, int oflag);
// 创建sem
sem_t *sem_open(const char *name, int oflag,
               mode_t mode, unsigned int value);
// oflag:与消息队列一样
// mode:与消息队列一样
// value:信号量的初始值,0~SEM_VALUE_MAX,表示资源的个数,使用资源用sem_wait,释放资源用sem_post

// 关闭sem
int sem_close(sem_t *sem);
// 进程终止或指向exec时,打开的有名sem会自动关闭,进程引用计数-1

// 删除sem
int sem_unlink(const char *name);

// 使用sem
int sem_wait(sem_t *sem); // 阻塞
int sem_trywait(sem_t *sem); // 非阻塞
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); // 指定时间之前阻塞
// 等待sem可用,将value减1,如果value > 0立即返回,否则阻塞。如果阻塞被信号中断,
// 返回EINTR,且无法通过SA_RESTART重启系统调用

// 释放sem
int sem_post(sem_t *sem);
// 将sem的值+1,如果多个进程处于sem_wait,唤醒一个

// 获取sem的值
int sem_getvalue(sem_t *sem, int *sval);
// 返回value的个数,如果有多个进程正在wait,返回0。但是该值返回的时候可能value的值已经改变。

Link with -pthread.

无名信号量API

上面说过无名信号量就是没有具名标识符,无法通过open打开使用。所以共享的条件是多个进程或线程可以看到同一块内存区域才能使用。线程最为合适,如果硬要给进程用,可以创建共享内存,然后将无名sem放到共享内存上。无名sem不使用 sem_open和sem_close、sem_unlink、sem_close,其余用法与有名sem相同。

// 初始化无名sem
int sem_init(sem_t *sem, int pshared, unsigned int value);
// value:0表示在线程间共享,大于0表示在进程间共享

// 销毁
int sem_destroy(sem_t *sem);
// 没有进程处于sem_wait状态时才可以被安全销毁

共享内存

共享内存可以在无关进程直接创建一块内存区域,让多个进程共同操作这块内存。POSIX共享内存同样采用文件类似的接口,也提供了标识符。可以动态的调整内存空间的大小。

mmap

我们经常用strace去看一个程序运行的系统调用,会看到大量的mmap和munmap的操作。例如在
线程的空间布局
里可以看到,线程栈的内容就是mmap来准备的。运行程序的时候,mmap会参与加载动态链接库等待。
mmap就是在调用进程的虚拟内存空间里创建一个内存映射,mmap分为:

  • 基于文件映射:将文件的一部分内容直接映射到进程的虚拟内存空间中,可以通过直接操作内存区域中的字节来操作文件
  • 匿名映射:没有实体文件与之关联,临时使用,匿名映射的内存区域会被初始化为0

进程有独立的内存空间,栈或者通过malloc分配的堆内存是彼此独立的。但是mmap创建的内存映射时,可以选择私有(MAP_PRIVATE)还是共享(MAP_SHARED):

  • MAP_PRIVATE:内存映射进程间独立,对于文件映射,内存字节的变更不会同步到磁盘上。
  • MAP_SHARED:发生改变时对拥有该共享内存的其他进程可见,对于文件映射,内存字节的改变会同步到磁盘上。

所以mmap可以分为4类:

  1. MAP_SHARED映射文件,内存对所有进程可见,且内存字节更改会同步到磁盘
  2. MAP_SHARED匿名映射,内存对所有进程可见
  3. MAP_PRIVATE映射文件,进程间不可见,内存字节更改不会同步到磁盘
  4. MA_PRIVATE匿名映射,进程间不可见(也是用了copy-on-write,发生了修改才复制新的页)

mmap API

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
// 解除映射
int munmap(void *addr, size_t length);

mmap参数:
addr:映射到内存的起始地址,设置NULL表示由系统决定
length、fd、offset:将文件fd作为映射源,从offset位置起,将长度为length的内容映射到内存
prot:表示对内存区域的操作保护,有以下几种

  • PROT_EXEC:映射的内容可执行
  • PROT_READ:映射的内容可读
  • PROT_WRITE:映射的内容可修改
  • PROT_NONE:映射的内容不可访问

flags:指定映射的类型

  • MAP_SHARED:创建共享映射
  • MAP_PRIVATE:创建私有映射
  • MAP_ANONYMOUS:创建匿名映射,fd必须设置为-1。
  • MAP_FIXED:表示必须把内容映射到对应的地址上,mmap操作的是页,addr和offset参数需要按页对齐

对于这些不同的映射形式,有如下几种使用场景:

  • 共享文件映射:在访问文件的时候,将磁盘的内容映射到内存空间中,Linux通过Page cache来缓存一部分映射,如果修改的这部分内存空间在Page cache上存在,则直接修改Page cache,否则再去读取磁盘文件,内核将修改过的页标记为脏页,在合适的时间写回到磁盘上。使用read和write时,除了磁盘->page cache,我们需要用户空间的buffer->pagecache或者pagecache->buffer,存在两次复制。使用mmap可以直接操作page cache,节省了一次数据复制,提升了性能
  • 私有文件映射:常用于动态链接库,多个进程共享库的文本信息,运行程序时可以看到有很多mmap的MAP_PRIVATE操作来加载动态链接库
  • 共享匿名映射:子进程可以继承这块区域,所以父子进程可以通过共享匿名映射来通信。共享匿名映射中的字节会被初始化为0,创建方式有两种:
    • flags指定MAP_ANONYMOUS,fd指定-1
    • open /dev/zero,然后将该fd传给mmap
  • 私有匿名映射:给进程分配一段私有的内存,无文件关联,独立访问。例如glibc中的malloc就是用mmap来实现的

共享内存API

  1. 创建共享内存
#include <sys/mman.h>
#include <sys/stat.h>        /* For mode constants */
#include <fcntl.h>           /* For O_* constants */

// 打开共享内存的文件描述符
int shm_open(const char *name, int oflag, mode_t mode);
// oflag:O_RDONLY、O_RDWR、O_CREAT、O_EXCL、O_TRUNC(将内存的size截断为0)
// mode:共享内存的使用权限,0表示只是打开

Link with -lrt.
  1. 创建好共享内存后,调整其大小
int ftruncate(int fd, off_t length);
  1. 调用mmap映射共享内存
// 查看共享内存的大小
int fstat(int fd, struct stat *statbuf);

// 调用mmap来做映射
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  1. 用完删除
// 删除共享内存,不会影响当前正在使用的映射,当所有的进程munmap解除映射,引用计数归0才删除
// 共享内存的数据具有内核持久性,即使所有进程都调用了munmap,没有unlink,那么这块区域就一直
// 存在,直到重启系统后消失
int shm_unlink(const char *name);

demo

通过client创建共享内存并打印字符串,通过server读取共享内存中的内容
client:

#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
  if (argc != 2) {
    printf("usage: %s /shmname\n", argv[1]);
    return 1;
  }
  const char *shmname = argv[1];
  int shmfd = shm_open(shmname, O_CREAT | O_EXCL | O_RDWR | O_TRUNC,
                       0666);  // 创建共享内存
  assert(shmfd != -1);
  if (ftruncate(shmfd, 1025) == -1) {  // 设置共享内存大小
    printf("resize shm failure\n");
    shm_unlink(shmname);
    return 1;
  }
  int ret;
  struct stat statbuf;
  ret = fstat(shmfd, &statbuf);  // 获取空闲内存大小
  assert(ret != -1);
  printf("shm length is %ld bytes\n", statbuf.st_size);
  char *shmptr;
  shmptr = (char *)mmap(NULL, statbuf.st_size, PROT_WRITE, MAP_SHARED, shmfd,
                        0);  // 通过mmap映射共享内存
  if (shmptr == MAP_FAILED) {
    printf("map shm failure\n");
    shm_unlink(shmname);
    return 1;
  }
  sprintf(shmptr, "%s", "hello world\n");
  sprintf(shmptr + 12, "%s", "hi\n");
  munmap(shmptr, statbuf.st_size);
  return 0;
}

server:

#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
  if (argc != 2) {
    printf("usage: %s /shmname\n", argv[1]);
    return 1;
  }
  const char *shmname = argv[1];

  int shmfd = shm_open(shmname, O_RDONLY, 0666);
  assert(shmfd != -1);
  char *shmptr;
  struct stat statbuf;
  int ret = fstat(shmfd, &statbuf);
  assert(ret != -1);
  printf("shm length is %ld bytes\n", statbuf.st_size);
  shmptr = (char *)mmap(NULL, statbuf.st_size, PROT_READ, MAP_SHARED, shmfd,0);
  if (shmptr == MAP_FAILED) {
    printf("map shm failure\n");
    return 1;
  }
  printf("%s", shmptr);
  munmap(shmptr, statbuf.st_size);
  return 0;
}

// ------------------
root@yielde:~/workspace/code-container/cpp/blog_demo# ./client /myshm
shm length is 1025 bytes

root@yielde:~/workspace/code-container/cpp/blog_demo# ./server /myshm
shm length is 1025 bytes
hello world
hi

学习自:
《UNIX环境高级编程》
《Linux环境编程从应用到内核》高峰 李彬 著

为什么搞个新协议?

2021年时,想为
Solon 生态
提供一种 MVC 体验的 Socket 和 WebSocket 开发方式。这个想法,要求消息“能路由”、“有元信息”、“可建立关联性”。于是就开发了 Socket.D 早期版本(算是草案版)。经过两年的实践,其重新定义为:

是想要有一种更简单、更通用的通讯方式。简单,且便适用任何场景和平台(想是这么想的啊)。而这,便以 Socket.D 协议作为载体。一个简单的、规范的,面向未来的网络应用协议。

为什么不凑合用别人的呢?

前人,总有不如意啊。而后人总是站在前人的成果上,吸取优点避开缺点。

协议 不称心的地方
http 单向通讯;只能同步响应
websocket 没有应用语义,只有框架;需要二次定制
rsocket 纯响应式接口太复杂;没有事件;元信息为二进制,无法固定标准。不通用
socket.io 没有流;没有元信息

Socket.D 具备它们的优点,又美好的避开了缺点。是,更具普世性的通用协议。

为什么不基于别人的呢?

Socket.D 作为网络应用协议,原则上可支持任意传输协议。目前适配有
TCP

UDP
之类的基础传输协议;也适配有
WebSocket

KCP
之类有加工过的传输协议。未来还可能适配别的传输协议。

为什么要基于事件消息驱动?

网络通信是异步的,消息驱动可建立起单个连接上的多路消息流,从而实现多路复用,一个连接同时多请求多响应。而基于事件,是让消息可路由,可分类处理。这个就像
mq
协议的 topic。

为什么要元信息?

http
协议,就是因为有元信息(它叫头信息),玩出了各种花!有了元信息,就可以为数据进行语义标注。就可以实现各种扩展的场景应用!

为什么要流?

连接上传输的数据即为流。协议通过流标识(sid),为传输来回的相关数据建立起关联性。Socket.D 基于流而行成的接口交互模型:

接口 描述 说明
send 发送 相当于 Qos0
sendAndRequest 发送并请求。要求一个答复 相当于 Qos1
sendAndSubscribe 发送并订阅。可接收零个或多个答复消息
reply 答复
replyEnd 答复结束

为什么是这样的接口交互?

首先 http 的接口交互是最经典。Socket.D 算是对它的学习、补充和扩展。因为我们是消息驱动的嘛,大家都是讲发消息、发消息。所以用 send 开头:

a) send 发送

发完后,不需要答复。它是能带来性能提升的,不仅是跳过了答复而节省网络使用,而且不需要等待响应或也不需要建立消息的流关联。是 http 请求/响应模式的补充。

b) sendAndRequest 发送并请求。要求一个答复

http 经典的请求/响应模式。不管在什么时候都非常有用,必须支持

c) sendAndSubscribe 发送并订阅。可接收多个答复消息

也是 http 请求/响应模式 的扩展,它允许多个答复消息被流回。可以看作是“collection”的响应,但不是将所有数据作为单个答复返回,而是将每个元素按顺序返回。

适用的场景可能是:

  • 获取视频列表
  • 获取目录中的产品
  • 逐行检索文件

d) reply 答复

配合 sendAndRequest,sendAndSubscribe 答复消息

e) replyEnd 答复结束

配合 sendAndSubscribe 答复消息,并告知答复结束了。

为什么规划了多平台多语言?

大型分布式系统通常由不同的团队使用各种技术和编程语言以模块化的方式实现。这些模块需要可靠地通信,支持快速、独立的进化。在分布式系统中,模块间有效且可扩展的通信是一个关键问题。它会显著影响用户体验的延迟以及构建和运行系统所需的资源量。

Socket.D 这么好的协议,必须争取让所有的平台和语言都能用上。参与这种问题的解决。

部署与发布:缺乏发布管理的部署活动对软件交付是低效的

部署和发布是软件工程中经常互换使用的两个术语,甚至感觉是等价的。然而,它们是不同的!

  • 部署是将软件从一个受控环境转移到另一个受控环境,
    它的目的是将软件从开发状态转化为生产状态,使得软件可以为用户提供服务。
  • 发布是将软件推向用户的过程
    ,应用程序需要多次更新、安全补丁和代码更改,跨平台和环境部署需要对版本进行适当的管理,
    有一定的计划性和管控因素。

部署是发布的前提,只有当软件已经成功部署后,才能进行发布。缺乏发布管理会导致发布不规则、手动交付过程、数据库更新问题、协作问题等。
如下,简单归纳了发布&部署的差异:
image.png

部署、发布:概念区分

日常研发活动中,我们会经常听到下面的说法,感觉有点差别,又感觉一头雾水说不清楚区别在哪里。

  • 功能还没集成进来。
  • 功能还没部署上去。
  • 功能还没交付。
  • 功能还没上线。
  • 功能还没发布。

下面对上述关键词进行了总结归纳

集成(Integrate)

  • 定义:将组成部分(部件)收集、归拢,并建立它们之间的联系或依赖关系,构建形成一个整体。
  • DoD:通过验证(验收)测试,确认集成的结果是正确的。
  • 说明:为了验证集成的结果是正确的,需要对它(集成的结果)进行验证(验收)测试,而为了做验证(验收),需要将它(集成的结果)部署到一个环境中。但验证(验收)测试、部署,这些活动并不是集成,它们只是为了验证集成达到了期望的结果。如果你能保证集成没有问题,那么可以不做部署和验收测试这2个动作。
  • 特征:将分散的东西合并到一起,形成一个整体
  • 举例:多个单元代码通过集成,形成一个组件。多个组件或模块通过集成,形成一个系统。多个系统通过集成,形成一个整体解决方案。

部署(Deploy)

  • 定义:安装、配置(如有)。
  • DoD:通过验证(验收)测试,确认部署的结果是正确的(成功部署)。
  • 说明:为了验证部署的结果是正确的,需要对它(部署的结果)进行验证(验收)测试。但验证(验收)测试并不是部署,它只是为了验证部署达到了期望的结果。如果你能保证部署没有问题,那么可以不做验收测试这个动作。
  • 特征:将软件“放置”到某个环境中。
  • 举例:部署人员将测试版本部署测试环境。将某个版本部署到试运行环境。将正式版本部署到生产环境。将一个模块部署到系统中。

交付(Delivery)

  • 定义:交给、付出、移交,指物(如软件安装包的光盘)或权(软件的管理权、使用权、所有权等)在人与人之间的转移、传递或接管。
  • DoD:接收方确认收到(如签收、同意)。
  • 说明:接收方可能会对收到的物或权进行验收测试,然后才签收。但验收测试、签收并不是交付,它只是为了验证交付的东西是期望的东西,它们是交付的下游动作。如果能保证交付物没有问题,那么可以不做验收测试、签收这样的动作。
  • 特征:交付物或权的拥有者发生了“转移”。
  • 举例:开发人员将测试版本交付給了测试人员。测试人员将正式版本交付给了运维人员。测试人员错误的将测试版本交付给了运维人员。IT团队将系统交付给了业务部。某公司将软件产品交付给了零售商。商店将软件光盘交付给了用户。这次只交付了一个模块。

上线(Go-live / Ship)

  • 定义:上到生成线,即部署到生产线上(生成环境中)
  • DoD:在生产环境中可以看到,并可以使用。
  • 说明:上线后,可以使用系统,也可以不使用系统。如果使用系统,开始创造业务价值,那么也叫投产(即投产=上线+使用系统)。如果上线后,不使用系统,那么表示系统还没有“开工”,它并不影响上线这个动作。“使用系统”这个动作是上线后的下游活动,它不是“上线”活动的一部分。
  • 特征:一定是部署到生产环境中(不是其它环境),即生产线上。上线 = 在生产环境上的部署。
  • 举例:IT部门上线了一个演示版。正式版刚刚上线,还没有用户访问。某系统上线了半年,还没有投产,某系统刚上线1个月,公司业绩就得到了大幅提升。

发布(Release)

  • 定义:将集成(构建)出来那个整体(发布对象),打上一个发布标签,提供出来,受众可以获得。
  • DoD:提供出来,受众可以获取到。
  • 说明:发布的版本未必是正式版,比如发布测试版(如Beta版)、试用版、演示版。发布之后,受众可以获得软件,但不一定就使用它。发布的软件可以存储在VCS(版本控制系统)中或制品库中,也可以存储在光盘等介质上。受众获得软件之后的下游动作,不一定是部署,也可能是其他动作(如交付或其他)。如:
    1. 发布测试版-->部署到测试环境-->交付给测试人员做验收测试。
    2. 发布正式版-->部署到生产环境-->交付给用户使用。
    3. 发布正式版产品(如windows安装盘)-->(售卖)交付给用户 --> 部署 -->上线使用。
  • 特征:发布物有(标签)标识,提供出来可以获得。
  • 举例:开发人员发布了一个测试版。开发团队在每个月都会发布一个演化版。某产品发布了新的版本,用户需要重新购买后,才能部署升级。某云平台发布了新的版本,不需要用户部署就可以使用新的功能。本次版本发布了,但没有人使用。

不同企业场景下的部署与发布

对于上面的概念解释,可能你会觉得有什么用呢?能解决什么问题呢?不妨按照以上的定义,把开头的那段话,进一步解读,得到如下信息:

  • 功能还没集成进来 --> 功能还没有被合并到一起,没有形成一个整体。
  • 功能还没部署上去 --> 功能还没有安装、配置到指定的环境中。
  • 功能还没交付 --> 功能还没有“转移”给使用者。
  • 功能还没上线 --> 功能还没有部署在生产环境。
  • 功能还没发布 --> 功能还没有提供出来,不可以获得。

除了集成是开发完成后首先完成的外,其它几个活动没有固定的依赖关系,它们的先后顺序需要根据具体的应用场景。

场景1-2B项目交付

某乙方公司为甲方公司开发了一个web应用,需部署到生产环境,再发布给甲方公司,交付给使用部门(用户),使用部门才能投产使用(上线),那么它们的先后顺序就是:集成—>部署—>发布—>交付—>上线。

场景2-2B在线服务类

A公司开发了一个SaaS应用,部署到生产环境,交付给B公司,B公司再加入自己公司的基础数据后上线了该SaaS应用,发布给使用部门(用户)使用,那么它们的先后顺序就是:集成—>部署—>交付—>上线—>发布。
还有更多场景,就不列举了。

场景3-2B软件售卖

A公司开发了一个商用软件,发布到网上,B公司通过购买获得,由A或B公司的技术员将软件部署到B公司的生产环境,交给B公司的使用部门(用户),使用部门才能投产使用(即上线),那么它们的先后顺序就是:集成—>发布—>部署—>交付—>上线。

场景4-2C软件包售卖

早年,微软发布了Window XP(存储在光盘中),交付给用户,用户再部署到生产环境,然后投产使用(上线)。现在的很多单体软件,大多也是这样的流程。那么它们的先后顺序就是:集成—>发布—>交付—>部署—>上线。

场景5-2C互联网在线服务

对于互联网应用服务,互联网厂商一般会进行集成(频率集成),通过自动化方式部署到dev/test/uat等环境,通过一定的审批机制获得部署到prod环境的授权(蓝绿、灰度等),正式发布上线,交付给用户使用
那么它们的先后顺序就是:集成—>部署—>发布—>上线—>交付
通过以上分析,你对“集成”、“部署”、“上线”、“交付”、“发布”的概念的理解是否清晰了?

吃透“部署发布”的重要性

上面说了这么多,目的不是为了死记某些概念,
而是想说明,不同组织、产品形态不同,部署发布方式差异很大,在设计持续交付 (CD) 流程之前,需要了解关于部署、发布的素有信息。

有助于设计并优化软件交付流程

pipeline (2).jpg
从代码提交到集成,再到功能验证,再到被部署到不同的环境,中间涉及了“代码提交信息”,“制品信息”,“环境配置信息”等,不同的发布方式,这些信息的传递和保存方式也各不相同。理解吃透这些差异,才能设计出来有意义的交付流程。

  • 如何集成涉及到了代码仓库的组织和构建流水线的设计
  • 部署又和环境紧密联系,还有部署策略
  • 上线又会和审批流程有关系
  • 发布就需要对制品进行晋级标签的处理
  • 交付就需要和制品的存储/分发方式密切相关

e7b070951c84b9abc645944d5542b68d.jpg

部署发布的质量取决于明确的发布计划

另外,发布管理是ITIL服务管理框架中的一个重要流程,主要负责计划、实施和控制IT服务的变更,确保变更过程中各个环节的顺利进行。
发布管理关注的是将经过测试并导入实际应用环境的新增或改进的配置项分发到最终用户,并确保这些配置项能够安全、可靠地运行。
因此需要在发布计划、包和构建发送进行测试之前进行广泛的规划,主要涉及

  • 发布单元具体业务需求及应用功能升级详情
  • 主要发布的发布数据
  • 预定义的文档流程
  • 广泛的测试计划

另外,
软件版本
需要适当的管理以避免与未来版本相关的问题。

相关案例

这里,从不同角度归纳一些关于发布的案例
。一般来说,需要提前制定发布计划,并且由专人(例如release manager这样的角色)负责管理发布计划。release manager 作为研发团队(研发执行)和客户(需求变更)之间的桥梁,清楚了解每次发布的变更,以及影响客户的范围。

案例-1 发布活动的协同

2016 年,联合航空为超过 1.43 亿用户提供服务。然而,软件发布管理是一个巨大的挑战。有几个手动流程和电子表格,这增加了发布周期时间。
因此,联合航空公司选择通过转移发布团队的角色来利用在岸/离岸发布模型,以确保完成特定的承诺。他们的发布管理方法最好的部分是使用 DevOps 和集中治理模型。他们设法通过持续交付团队 (CDT) 和开发团队之间的协作来简化发布。

案例-2 Firefox的发布火车

Firefox的发布流程:每个独立的发布火车(新的发布过程采用火车模型,固定的“发车”时间,特性的发布取决于该特性是否赶上最近的火车发车时间)包括
6周的开发时间加上12周的稳定时间:

除了发布计划,这里也需要
分支策略的配合
(新的开发成果不会直接发布到Aurora和Beta分支上,这些分支需要被开发人员和社区测试人员共同测试完方可;如果发现开发中存在程序问题或者BUG,就需要先解决问题)

案例-3 支持发布的平台Zadig

对于发布,市场上少有平台会关注这个环节。笔者过去见过的团队,一般都会用一个excel表格的方式来记录各个版本的变更,以及发布的客户范围。
这里介绍
Zadig平台中的“
发布管理
”模块,
特别是对于2B场景,可能面对很多不同客户,包括不同的定制, 需要一个平台来汇总这些信息,包括

  • 发布版本管理-与产品版本规规划对齐,首尾呼应,形成闭环

  • 发布审批 - 对于直接对线上的正式环境,需要配合自动化的流水线,取得管理人员的审批

image.png
image.png

  • 客户管理- 最终的交付物最终给哪个客户,需要明确的体现出来

目前,Zadig更多是针对于客户SAAS服务,直接面对线上环境,所以还会有线上基础设施云供应商的配置。
这里其实可以拓展更多,比如对于私有化部署场景,这里交付的可能是部署包,数据库文件等等。

image.png

最后

上面从部署发布的概念,不同场景,到案例工具进行了总结,希望能对大家有所启发~
下面归纳了可能影响发布的关键要素
。部署发布是软件交付的最后一公里,呼应了产品的发布计划,有序的发布管理和流程,会让价值交付更加清晰透明,取代混乱和低效。

  • 版本发布计划/需求变更
  • 版本发布流程/人员
  • 分支策略
  • 部署策略
  • 环境/配置管理
  • 制品晋级/标签

注:部分内容参考网络资料,如有侵权请联系我