在Ubuntu上用sane api实现通用扫描功能

最近由于工作需要,要写一套扫描相关的接口。

在这里记录一下,实现还有有点复杂的。

目录

依赖

主要功能

初始化

获取当前扫描仪列表

打开扫描仪

sane_open

设置扫描选项

sane_control_option

扫描

关闭设备

结束使用

参考资料


依赖

复制代码
sudo apt install libsane-dev  sane-utils

主要功能

初始化

我们在操作扫描仪之前需要初始化才能正常使用。

初始化使用的是sane里的sane_init。

复制代码
void scanner_init()
{
  printf("[%s] Start\n", __FUNCTION__);
  SANE_Int version_code = 0;
  sane_init(&version_code, auth_callback);
  printf("SANE version code: %d\n", version_code);
}

static void
auth_callback(SANE_String_Const resource,
              SANE_Char *username, SANE_Char *password)
{
}

初始化成功则version_code为SANE_STATUS_GOOD(0)。

获取当前扫描仪列表

核心是sane_get_devices函数。

先通过sane_get_devices获取扫描仪列表,然后申请一个二维数组,将扫描仪列表放入二维数组中返回。

复制代码
const char **scanner_get_available_list()
{
  printf("[%s] Start\n", __FUNCTION__);
  SANE_Status status;
  SANE_Int num_devices = 0;
  const SANE_Device **device_list;

  //获取扫描仪列表
  status = sane_get_devices(&device_list, SANE_FALSE);
  if (status != SANE_STATUS_GOOD)
  {
    printf("Error getting device list: %s\n", sane_strstatus(status));
    return NULL;
  }

  // 获取当前设备数量
  while (device_list[num_devices])
    num_devices++;

  // 如果设备列表为空,返回
  if (num_devices == 0)
  {
    printf("No scanners found.\n");
    sane_exit();
    return NULL;
  }

  // 分配内存
  const char **scanner_list = malloc(sizeof(SANE_Device *) * (num_devices + 1));
  if (!scanner_list)
  {
    printf("Failed to allocate memory.\n");
    sane_exit();
    return NULL;
  }

  // 继续分配内存
  for (int i = 0; i < num_devices; i++)
  {
    scanner_list[i] = strdup(device_list[i]->name);
    if (!scanner_list[i])
    {
      printf("Failed to allocate memory.\n");
      for (int j = 0; j < i; j++)
      {
        free(scanner_list[j]);
      }
      free(scanner_list);
      sane_exit();
      return NULL;
    }
  }
  scanner_list[num_devices] = NULL;

  // 返回设备列表
  return scanner_list;
}

我们再把获取到的设备列表循环打印一下。

复制代码
    const char **scanner_list = scanner_get_available_list();

    if (scanner_list != NULL)
    {
        for (int num_devices = 0; scanner_list[num_devices]; ++num_devices)
        {
            if (scanner_list[num_devices] != NULL) // 添加一个检查
            {
                printf("Device %d: name=%s \n",
                       num_devices, scanner_list[num_devices]);
            }
        }
    }
    else
    {
        return;
    }

打开扫描仪

sane_open

这里介绍一下sane_open,第一个参数是扫描仪的名称,第二个参数是一个空的句柄,打开后通过句柄进行后续操作。

复制代码
extern SANE_Status sane_open (SANE_String_Const devicename,
			      SANE_Handle * handle);

函数的具体实现如下:

复制代码
static SANE_Handle sane_handle = NULL; // 扫描仪设备句柄,全局变量

int scanner_open_device(char *scanner_name)
{
  printf("[%s] Start\n", __FUNCTION__);
  SANE_Status sane_status = 0;

  if (sane_status = sane_open(scanner_name, &sane_handle))
  {
    printf("sane_open status: %s\n", sane_strstatus(sane_status));
  }
  if (sane_status != SANE_STATUS_GOOD)
    sane_handle = NULL;

  return sane_status;
}

这里的入参scanner_name是扫描仪列表的scanner_list,如果要打开第一个扫描仪的话是scanner_list0

如果函数的返回值不是SANE_STATUS_GOOD,表示打开失败了。


设置扫描选项

sane_control_option

扫描的所有参数都是通过sane_control_option实现的,每个参数的功能详见备注。

复制代码
extern SANE_Status sane_control_option (SANE_Handle handle,  //sane_open的句柄
                                        SANE_Int option,     //要设置选项的序号,2是颜色,3是分辨率
					                    SANE_Action action,  //操作的类型,给选项赋值或者获取当前值
                                        void *value,         //value的实际值
					                    SANE_Int *info);     //没发现有什么用

操作的类型一共有以下三种,我们这里只用到第二种。

复制代码
typedef enum
  {
    SANE_ACTION_GET_VALUE = 0,
    SANE_ACTION_SET_VALUE,
    SANE_ACTION_SET_AUTO
  }
SANE_Action;

我这里设置了颜色,扫描的分辨率和纸张大小,还有很多可以设置的选项,可以自行探索。

复制代码
static SANE_Handle sane_handle = NULL; // 扫描仪设备句柄,全局变量

// 设置指定扫描仪颜色,通过传入参数val_color进行设置扫描设备的颜色
int scanner_set_color(SANE_String val_color)
{
  printf("[%s] Start!\n", __FUNCTION__);
  SANE_Status status;
  status = sane_control_option(sane_handle, 2,
                               SANE_ACTION_SET_VALUE, val_color, NULL);

  if (status != SANE_STATUS_GOOD)
  {
    printf("Option did not set, desc = %s\n", sane_strstatus(status));

    return status;
  }

  printf("set color option success!\n");
  return status;
}


// 设置指定扫描仪扫描的分辨率(清晰程度,分辨率越大越清晰)
int scanner_set_resolutions(SANE_Int val_resolution)
{
  printf("[%s] Start!\n", __FUNCTION__);
  SANE_Status status;

  status = sane_control_option(sane_handle, 3, SANE_ACTION_SET_VALUE, &val_resolution, NULL);
  if (status != SANE_STATUS_GOOD)
  {
    printf("Option did not set, desc = %s\n", sane_strstatus(status));
    return status;
  }

  printf("set resolution option success!\n");
  return status;
}

设置纸张大小有点复杂,因为纸张大小没有对应的option。因此曲线救国,通过设置扫描的纸张的长和宽来实现。长和宽的option序号分别是9和10。

复制代码
enum sizes_type
{
  A2 = 1,
  A3,
  A4,
  A5,
  A6
};

static double g_saneSizeA4BrY = 297;

int scanner_set_size(SANE_String size)
{
  printf("[%s] Start!\n", __FUNCTION__);
  SANE_Status status = SANE_STATUS_GOOD;
  int type;

  if (!strcmp("A2", size))
  {
    type = A2;
  }
  else if (!strcmp("A3", size))
  {
    type = A3;
  }
  else if (!strcmp("A4", size))
  {
    type = A4;
  }
  else if (!strcmp("A5", size))
  {
    type = A5;
  }
  else if (!strcmp("A6", size))
  {
    type = A6;
  }
  else
  {
    type = 0;
  }

  switch (type)
  {
  case A2:
    status = kdk_scanner_set_size_real(sane_handle, 420, 594);
    break;
  case A3:
    status = kdk_scanner_set_size_real(sane_handle, 297, 420);
    break;
  case A4:
    status = kdk_scanner_set_size_real(sane_handle, 210, g_saneSizeA4BrY);
    break;
  case A5:
    status = kdk_scanner_set_size_real(sane_handle, 148, 210);
    break;
  case A6:
    status = kdk_scanner_set_size_real(sane_handle, 105, 144);
    break;
  default:
    status = SANE_STATUS_UNSUPPORTED;
  }

  return status;
}

/**
 * @brief scanner_set_size_real 统一设置扫描设备尺寸
 *
 * @param sane_handle 扫描句柄
 *
 * @param val_size_br_x 扫描设备右下角的x坐标
 *
 * @param val_size_br_y 扫描设备右下角的y坐标
 *
 * @return 返回扫描设备设置尺寸的情况
 */
SANE_Status scanner_set_size_real(SANE_Handle sane_handle, SANE_Int val_size_br_x,
                                      SANE_Int val_size_br_y)
{
  printf("[%s] Start!\n", __FUNCTION__);
  SANE_Status status = SANE_STATUS_GOOD;

  SANE_Word x = SANE_FIX(val_size_br_x);
  SANE_Word y = SANE_FIX(val_size_br_y);

  SANE_Word zero = SANE_FIX(0.0);
  status = sane_control_option(sane_handle, 7, SANE_ACTION_SET_VALUE, &zero, NULL);
  if (status != SANE_STATUS_GOOD)
  {
    return status;
  }

  status = sane_control_option(sane_handle, 8, SANE_ACTION_SET_VALUE, &zero, NULL);
  if (status != SANE_STATUS_GOOD)
  {
    return status;
  }

  status = sane_control_option(sane_handle, 9,
                               SANE_ACTION_SET_VALUE, &x, NULL);

  if (status != SANE_STATUS_GOOD)
  {
    printf("Option x did not set, desc = %s\n", sane_strstatus(status));
   
    return status;
  }

  status = sane_control_option(sane_handle, 10,
                               SANE_ACTION_SET_VALUE, &y, NULL);

  if (status != SANE_STATUS_GOOD)
  {
    printf("Option y did not set, desc = %s\n", sane_strstatus(status));
     
    return status;
  }

  return status;
}

和前面一样,如果函数的返回值不是SANE_STATUS_GOOD,表示失败了。

扫描

扫描我分成两类,分为单页单面扫描和多页双面扫描。

单页和多页也是一种可以设置的扫描属性,单页和多页的主要是这个这个属性的区别,别的部分都差不多。

复制代码
//设置扫描是单页还是多页
int scanner_set_page_type(SANE_Int type)
{
  printf("[%s] Start!\n", __FUNCTION__);
  SANE_Status status;

  //对应的option序号为4
  status = sane_control_option(sane_handle, 4,
                               SANE_ACTION_SET_VALUE, &type, NULL);

  if (status != SANE_STATUS_GOOD)
  {
    printf("Option did not set, desc = %s\n", sane_strstatus(status));

    return status;
  }

  printf("set page type option success!\n");
  return status;
}

单页单面扫描,就是不管有多少页都只扫描第一页的第一面。

复制代码
  /**
   * @brief 指定扫描仪进行扫描(统一按照多页,双面处理)
   *
   * @param fileName:保存扫描文件的文件名,比如传test的话,扫描后的文件会是test_1,test_2之类的形式
   * 
   * @param type:扫描类型 0:单面单面扫描,1:多页双面扫描
   *
   * @return 操作的返回值,0或者7为成功,其他为失败
   */
int scanner_start_scan(SANE_String_Const fileName, int type)
{
  printf("[%s] Start\n", __FUNCTION__);
  SANE_Status sane_status = 0;
 
  switch (type)
  {
  case 0:
    return do_scan_one(fileName);

  case 1:
    return do_scan_all(fileName);

  default:
    return do_scan_all(fileName);
  }
}

这是扫描单页的接口

复制代码
// 单页扫描
SANE_Status do_scan_one(const char *fileName)
{
  printf("[%s] Start\n", __FUNCTION__);

  del_old_pic();//扫描之前删掉上一次的内容

  SANE_Status status;
  FILE *ofp = NULL;
  char path[PATH_MAX];
  char part_path[PATH_MAX];
  buffer_size = (32 * 1024);
  buffer = (SANE_Byte *)malloc(buffer_size);
  int i = 1;

  // 设置打印机单页进纸张
  status = kdk_scanner_set_page_type(1);
  if (status != SANE_STATUS_GOOD)
  {
      printf("set page type fail:%s\n", sane_strstatus(status));
      return status;
  }

  do
  {
    // 设置保存路径
    sprintf(path, "/tmp/%s-%d.pnm", fileName, i); // 格式化PNM文件路径
    strcpy(part_path, path);
    strcat(part_path, ".part");

    printf("picture name: %s\n", path);

    // 开始扫描
    status = sane_start(sane_handle);
    if (status != SANE_STATUS_GOOD)
    {
      break;
    }

    if (NULL == (ofp = fopen(part_path, "w")))
    {
      status = SANE_STATUS_ACCESS_DENIED;
      break;
    }

    // 保存扫描数据
    status = scan_it(ofp);

    switch (status)
    {
    case SANE_STATUS_GOOD:
    case SANE_STATUS_EOF:
    {
      status = SANE_STATUS_GOOD;
      if (!ofp || 0 != fclose(ofp))
      {
        status = SANE_STATUS_ACCESS_DENIED;
        break;
      }
      else
      {
        ofp = NULL;
        if (rename(part_path, path))
        {
          status = SANE_STATUS_ACCESS_DENIED;
          break;
        }
      }
    }
    break;
    default:
      break;
    }
  } while (0);

 
  if (ofp)
  {
    fclose(ofp);
    ofp = NULL;
  }
  if (buffer)
  {
    free(buffer);
    buffer = NULL;
  }

  return status;
}

// 删除上一次扫描的文件
void del_old_pic()
{
  DIR *dir;
  struct dirent *entry;
  char path[] = "/tmp/";
  char ext[] = ".pnm";

  dir = opendir(path);
  if (dir == NULL)
  {
    perror("Unable to open directory");
    exit(EXIT_FAILURE);
  }

  while ((entry = readdir(dir)) != NULL)
  {
    // Check if the entry is a file and ends with .pnm
    if (entry->d_type == DT_REG &&
        strstr(entry->d_name, ext) != NULL &&
        strcmp(entry->d_name + strlen(entry->d_name) - strlen(ext), ext) == 0)
    {
      char full_path[512];
      sprintf(full_path, "%s%s", path, entry->d_name);

      if (remove(full_path) == 0)
      {
        printf("Deleted %s\n", full_path);
      }
      else
      {
        perror("Unable to delete file");
      }
    }
  }

  closedir(dir);
}

// sane 设置扫描方式
int kdk_scanner_set_page_type(SANE_Int type)
{
  printf("[%s] Start!\n", __FUNCTION__);
  SANE_Status status;
  status = sane_control_option(sane_handle, 4,
                               SANE_ACTION_SET_VALUE, &type, NULL);

  if (status != SANE_STATUS_GOOD)
  {
    printf("Option did not set, desc = %s\n", sane_strstatus(status));

    return status;
  }

  printf("set page type option success!\n");
  return status;
}

保存图片,这一部分细节很多,我也没仔细研究,直接用就行。

复制代码
static SANE_Status scan_it(FILE *ofp)
{
  int i, len, first_frame = 1, offset = 0, must_buffer = 0, hundred_percent;
  SANE_Byte min = 0xff, max = 0;
  SANE_Parameters parm;
  SANE_Status status;
  Image image = {0, 0, 0, 0, 0};
  static const char *format_name[] = {"gray", "RGB", "red", "green", "blue"};
  SANE_Word total_bytes = 0, expected_bytes;
  SANE_Int hang_over = -1;

  do
  {

    if (!first_frame)
    {
      status = sane_start(sane_handle);
      if (status != SANE_STATUS_GOOD)
      {
        goto cleanup;
      }
    }

    status = sane_get_parameters(sane_handle, &parm);
    if (status != SANE_STATUS_GOOD)
    {
      goto cleanup;
    }

    if (first_frame)
    {
      switch (parm.format)
      {
      case SANE_FRAME_RED:
      case SANE_FRAME_GREEN:
      case SANE_FRAME_BLUE:
        assert(parm.depth == 8);
        must_buffer = 1;
        offset = parm.format - SANE_FRAME_RED;
        break;
      case SANE_FRAME_RGB:
        assert((parm.depth == 8) || (parm.depth == 16));
      case SANE_FRAME_GRAY:
        assert((parm.depth == 1) || (parm.depth == 8) || (parm.depth == 16));
        if (parm.lines < 0)
        {
          must_buffer = 1;
          offset = 0;
        }
        else
        {
          write_pnm_header(parm.format, parm.pixels_per_line, parm.lines, parm.depth, ofp);
        }
        break;
      default:
        break;
      }

      if (must_buffer)
      {
        image.width = parm.bytes_per_line;
        if (parm.lines >= 0)
          image.height = parm.lines - STRIP_HEIGHT + 1;
        else
          image.height = 0;

        image.x = image.width - 1;
        image.y = -1;
        if (!advance(&image))
        {
          status = SANE_STATUS_NO_MEM;
          goto cleanup;
        }
      }
    }
    else
    {
      assert(parm.format >= SANE_FRAME_RED && parm.format <= SANE_FRAME_BLUE);
      offset = parm.format - SANE_FRAME_RED;
      image.x = image.y = 0;
    }

    hundred_percent = parm.bytes_per_line * parm.lines * ((parm.format == SANE_FRAME_RGB || parm.format == SANE_FRAME_GRAY) ? 1 : 3);

    // 这段是写图片数据
    while (1)
    {
      double progr;
      status = sane_read(sane_handle, buffer, buffer_size, &len);
      total_bytes += (SANE_Word)len;
      progr = ((total_bytes * 100.) / (double)hundred_percent);
      if (progr > 100.)
        progr = 100.;

      if (status != SANE_STATUS_GOOD)
      {
        if (status != SANE_STATUS_EOF)
        {
          return status;
        }
        break;
      }

      if (must_buffer)
      {
        switch (parm.format)
        {
        case SANE_FRAME_RED:
        case SANE_FRAME_GREEN:
        case SANE_FRAME_BLUE:
          for (i = 0; i < len; ++i)
          {
            image.data[offset + 3 * i] = buffer[i];
            if (!advance(&image))
            {
              status = SANE_STATUS_NO_MEM;
              goto cleanup;
            }
          }
          offset += 3 * len;
          break;
        case SANE_FRAME_RGB:
          for (i = 0; i < len; ++i)
          {
            image.data[offset + i] = buffer[i];
            if (!advance(&image))
            {
              status = SANE_STATUS_NO_MEM;
              goto cleanup;
            }
          }
          offset += len;
          break;
        case SANE_FRAME_GRAY:
          for (i = 0; i < len; ++i)
          {
            image.data[offset + i] = buffer[i];
            if (!advance(&image))
            {
              status = SANE_STATUS_NO_MEM;
              goto cleanup;
            }
          }
          offset += len;
          break;
        default:
          break;
        }
      }
      else /* ! must_buffer */
      {
        if ((parm.depth != 16))
          fwrite(buffer, 1, len, ofp);
        else
        {
#if !defined(WORDS_BIGENDIAN)
          int i, start = 0;
          /* check if we have saved one byte from the last sane_read */
          if (hang_over > -1)
          {
            if (len > 0)
            {
              fwrite(buffer, 1, 1, ofp);
              buffer[0] = (SANE_Byte)hang_over;
              hang_over = -1;
              start = 1;
            }
          }
          /* now do the byte-swapping */
          for (i = start; i < (len - 1); i += 2)
          {
            unsigned char LSB;
            LSB = buffer[i];
            buffer[i] = buffer[i + 1];
            buffer[i + 1] = LSB;
          }
          /* check if we have an odd number of bytes */
          if (((len - start) % 2) != 0)
          {
            hang_over = buffer[len - 1];
            len--;
          }
#endif
          fwrite(buffer, 1, len, ofp);
        }
      }

      if (verbose && parm.depth == 8)
      {
        for (i = 0; i < len; ++i)
          if (buffer[i] >= max)
            max = buffer[i];
          else if (buffer[i] < min)
            min = buffer[i];
      }
    }
    first_frame = 0;
  } while (!parm.last_frame);

  if (must_buffer)
  {
    image.height = image.y;
    write_pnm_header(parm.format, parm.pixels_per_line, image.height, parm.depth, ofp);

#if !defined(WORDS_BIGENDIAN)
    if (parm.depth == 16)
    {
      int i;
      for (i = 0; i < image.height * image.width; i += 2)
      {
        unsigned char LSB;
        LSB = image.data[i];
        image.data[i] = image.data[i + 1];
        image.data[i + 1] = LSB;
      }
    }
#endif
    fwrite(image.data, 1, image.height * image.width, ofp);
  }

  fflush(ofp);

cleanup:
  if (image.data)
    free(image.data);

  return status;
}

void write_pnm_header(SANE_Frame format, int width, int height, int depth, FILE *ofp)
{
  printf("[%s] Start\n", __FUNCTION__);
  switch (format)
  {
  case SANE_FRAME_RED:
  case SANE_FRAME_GREEN:
  case SANE_FRAME_BLUE:
  case SANE_FRAME_RGB:
    fprintf(ofp, "P6\n# SANE data follows\n%d %d\n%d\n", width, height, (depth <= 8) ? 255 : 65535);
    break;
  default:
    if (depth == 1)
      fprintf(ofp, "P4\n# SANE data follows\n%d %d\n", width, height);
    else
      fprintf(ofp, "P5\n# SANE data follows\n%d %d\n%d\n", width, height, (depth <= 8) ? 255 : 65535);
    break;
  }
}

static void *
advance(Image *image)
{
  if (++image->x >= image->width)
  {
    image->x = 0;
    if (++image->y >= image->height || !image->data)
    {
      size_t old_size = 0, new_size;

      if (image->data)
        old_size = image->height * image->width;

      image->height += STRIP_HEIGHT;
      new_size = image->height * image->width;

      if (image->data)
        image->data = realloc(image->data, new_size);
      else
        image->data = malloc(new_size);
      if (image->data)
        memset(image->data + old_size, 0, new_size - old_size);
    }
  }
  if (!image->data)
    fprintf(stderr, "can't allocate image buffer (%dx%d)\n",
            image->width, image->height);
  return image->data;
}

双页扫描,用do_scan_all替换do_scan_one,其他的函数都一样。

复制代码
// 双面扫描全部文件+保存为PNM图像格式
SANE_Status do_scan_all(const char *fileName)
{
  printf("[%s] Start\n", __FUNCTION__);

  SANE_Status status;           // 返回状态
  FILE *ofp = NULL;             // 输出文件指针
  char path[PATH_MAX];          // PNM文件路径
  char part_path[PATH_MAX];     // 临时PNN文件路径
  buffer_size = (32 * 1024);    // 缓冲区大小
  buffer = malloc(buffer_size); // 动态分配缓冲区
  int i = 1;

  del_old_pic();

  //设置打印机多页进纸张
  status = kdk_scanner_set_page_type(0);
  if (status != SANE_STATUS_GOOD)
  {
      printf("set page type fail:%s\n", sane_strstatus(status));
      return status;
  }

  do
  {
    sprintf(path, "/tmp/%s-%d.pnm", fileName, i); // 格式化PNM文件路径
    strcpy(part_path, path);                      // 复制PNM文件路径到临时文件路径
    strcat(part_path, ".part");                   // 在临时文件路径后添加扩展名".part"

    // 启动扫描过程
    status = sane_start(sane_handle);
    if (status != SANE_STATUS_GOOD)
    {
      break;
    }

    // 创建临时文件
    if (NULL == (ofp = fopen(part_path, "w")))
    {
      status = SANE_STATUS_ACCESS_DENIED;
      break;
    }

    // 进行扫描,并将结果写入到临时文件中
    status = scan_it(ofp);

    switch (status)
    {
    case SANE_STATUS_GOOD:
    case SANE_STATUS_EOF:
    {
      // 扫描成功或结束
      status = SANE_STATUS_GOOD;

      // 关闭临时文件,并检查是否成功关闭
      if (!ofp || 0 != fclose(ofp))
      {
        status = SANE_STATUS_ACCESS_DENIED;
        break;
      }
      else
      {
        ofp = NULL; // 将文件指针设置为NULL,避免重复关闭
        // 将临时文件重命名为正式的PNM文件
        if (rename(part_path, path))
        {
          status = SANE_STATUS_ACCESS_DENIED;
          break;
        }
      }
    }
    break;
    default:
      break;
    }
    i++;
  } while (status == SANE_STATUS_GOOD);

  // 如果出现错误,则取消扫描进程
  if (SANE_STATUS_GOOD != status)
  {
    sane_cancel(sane_handle);
  }

  // 关闭输出文件
  if (ofp)
  {
    fclose(ofp);
    ofp = NULL;
  }

  // 释放缓冲区内存
  if (buffer)
  {
    free(buffer);
    buffer = NULL;
  }

  if ((status == SANE_STATUS_NO_DOCS) && (i > 1))
    status = SANE_STATUS_GOOD;

  return status; // 返回状态
}

扫描完成会会在tmp下生成扫描文件。

关闭设备

复制代码
void scanner_close_device()
{
  printf("[%s] Start\n", __FUNCTION__);
  if (sane_handle != NULL)
  {
    sane_close(sane_handle);
  }
  sane_handle = NULL;
}

结束使用

复制代码
void scanner_exit()
{
  printf("[%s] Start\n", __FUNCTION__);
  sane_exit();
}

参考资料

Linux下通用扫描仪API------SANE( Scanner Access Now Easy)_linux sane-CSDN博客

相关推荐
A小辣椒16 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒20 小时前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式