C库函数

逆向笔记 · 03-20 · 188 人浏览

1.memset()

描述

C 库函数 void memset(void str, int c, size_t n) 复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。

声明

下面是 memset() 函数的声明。

void memset(void str, int c, size_t n)

参数

  • str -- 指向要填充的内存块。
  • c -- 要被设置的值。该值以 int 形式传递,但是函数在填充内存块时是使用该值的无符号字符形式。
  • n -- 要被设置为该值的字符数。

返回值

该值返回一个指向存储区 str 的指针。

实例

下面的实例演示了 memset() 函数的用法。

include <stdio.h>

include <string.h>

int main ()
{
char str[50];

strcpy(str,"This is string.h library function");
puts(str);

memset(str,'#',7);
puts(str);

return(0);
}

让我们编译并运行上面的程序,这将产生以下结果:

This is string.h library function

is string.h library function

2.strcpy()

描述

C 库函数 char strcpy(char dest, const char *src) 把 src 所指向的字符串复制到 dest

需要注意的是如果目标数组 dest 不够大,而源字符串的长度又太长,可能会造成缓冲溢出的情况。

声明

下面是 strcpy() 函数的声明。

char strcpy(char dest, const char *src)

参数

  • dest -- 指向用于存储复制内容的目标数组。
  • src -- 要复制的字符串。

返回值

该函数返回一个指向最终的目标字符串 dest 的指针。

实例

下面的实例演示了 strcpy() 函数的用法。

include <stdio.h>

include <string.h>

int main()
{
char src[40];
char dest[100];

memset(dest, '\0', sizeof(dest));
strcpy(src, "This is runoob.com");
strcpy(dest, src);

printf("最终的目标字符串: %s\n", dest);

return(0);
}
最终的目标字符串: This is runoob.com

3.strcat()

描述

C 库函数 char strcat(char dest, const char *src) 把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。

声明

下面是 strcat() 函数的声明。

char strcat(char dest, const char *src)

参数

  • dest -- 指向目标数组,该数组包含了一个 C 字符串,且足够容纳追加后的字符串。
  • src -- 指向要追加的字符串,该字符串不会覆盖目标字符串。

返回值

该函数返回一个指向最终的目标字符串 dest 的指针。

实例

下面的实例演示了 strcat() 函数的用法。

实例

include <stdio.h>

include <string.h>

int main ()
{
char src[50], dest[50];

strcpy(src, "This is source");
strcpy(dest, "This is destination");

strcat(dest, src);

printf("最终的目标字符串: |%s|", dest);

return(0);
}

让我们编译并运行上面的程序,这将产生以下结果:

最终的目标字符串: |This is destinationThis is source|

4.strcmp()

描述

C 库函数 int strcmp(const char str1, const char str2) 把 str1 所指向的字符串和 str2 所指向的字符串进行比较。

声明

下面是 strcmp() 函数的声明。

int strcmp(const char str1, const char str2)

参数

  • str1 -- 要进行比较的第一个字符串。
  • str2 -- 要进行比较的第二个字符串。

返回值

该函数返回值如下:

  • 如果返回值小于 0,则表示 str1 小于 str2。
  • 如果返回值大于 0,则表示 str1 大于 str2。
  • 如果返回值等于 0,则表示 str1 等于 str2。

实例

下面的实例演示了 strcmp() 函数的用法。

include <stdio.h>

include <string.h>

int main ()
{
char str1[15];
char str2[15];
int ret;

strcpy(str1, "abcdef");
strcpy(str2, "ABCDEF");

ret = strcmp(str1, str2);

if(ret < 0)
{

  printf("str1 小于 str2");

}
else if(ret > 0)
{

  printf("str1 大于 str2");

}
else
{

  printf("str1 等于 str2");

}

return(0);
}
让我们编译并运行上面的程序,这将产生以下结果:

str1 大于 str2

5.sprintf()

描述

C 库函数 int sprintf(char str, const char format, ...) 发送格式化输出到 str 所指向的字符串。

声明

下面是 sprintf() 函数的声明。

int sprintf(char str, const char format, ...)

参数

  • str -- 这是指向一个字符数组的指针,该数组存储了 C 字符串。
  • format -- 这是字符串,包含了要被写入到字符串 str 的文本。它可以包含嵌入的 format 标签,format 标签可被随后的附加参数中指定的值替换,并按需求进行格式化。format 标签属性是 %flags.precisionspecifier,具体讲解如下:
specifier(说明符)输出
c字符
d 或 i有符号十进制整数
e使用 e 字符的科学科学记数法(尾数和指数)
E使用 E 字符的科学科学记数法(尾数和指数)
f十进制浮点数
g自动选择 %e 或 %f 中合适的表示法
G自动选择 %E 或 %f 中合适的表示法
o有符号八进制
s字符的字符串
u无符号十进制整数
x无符号十六进制整数(小写字母)
X无符号十六进制整数(大写字母)
p指针地址
n无输出
%字符
flags(标识)描述
-在给定的字段宽度内左对齐,默认是右对齐(参见 width 子说明符)。
+强制在结果之前显示加号或减号(+ 或 -),即正数前面会显示 + 号。默认情况下,只有负数前面会显示一个 - 号。
(space)如果没有写入任何符号,则在该值前面插入一个空格。
#与 o、x 或 X 说明符一起使用时,非零值前面会分别显示 0、0x 或 0X。
与 e、E 和 f 一起使用时,会强制输出包含一个小数点,即使后边没有数字时也会显示小数点。默认情况下,如果后边没有数字时候,不会显示显示小数点。
与 g 或 G 一起使用时,结果与使用 e 或 E 时相同,但是尾部的零不会被移除。
0在指定填充 padding 的数字左边放置零(0),而不是空格(参见 width 子说明符)。
width(宽度)描述
(number)要输出的字符的最小数目。如果输出的值短于该数,结果会用空格填充。如果输出的值长于该数,结果不会被截断。
*宽度在 format 字符串中未指定,但是会作为附加整数值参数放置于要被格式化的参数之前。
.precision(精度)描述
.number对于整数说明符(d、i、o、u、x、X):precision 指定了要写入的数字的最小位数。如果写入的值短于该数,结果会用前导零来填充。如果写入的值长于该数,结果不会被截断。精度为 0 意味着不写入任何字符。
对于 e、E 和 f 说明符:要在小数点后输出的小数位数。
对于 g 和 G 说明符:要输出的最大有效位数。
对于 s: 要输出的最大字符数。默认情况下,所有字符都会被输出,直到遇到末尾的空字符。
对于 c 类型:没有任何影响。
当未指定任何精度时,默认为 1。如果指定时不带有一个显式值,则假定为 0。
.*精度在 format 字符串中未指定,但是会作为附加整数值参数放置于要被格式化的参数之前。
length(长度)描述
h参数被解释为短整型或无符号短整型(仅适用于整数说明符:i、d、o、u、x 和 X)。
l参数被解释为长整型或无符号长整型,适用于整数说明符(i、d、o、u、x 和 X)及说明符 c(表示一个宽字符)和 s(表示宽字符字符串)。
L参数被解释为长双精度型(仅适用于浮点数说明符:e、E、f、g 和 G)。
  • 附加参数 -- 根据不同的 format 字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了 format 参数中指定的每个 % 标签。参数的个数应与 % 标签的个数相同。

返回值

如果成功,则返回写入的字符总数,不包括字符串追加在字符串末尾的空字符。如果失败,则返回一个负数。

实例

下面的实例演示了 sprintf() 函数的用法。

include <stdio.h>

include <math.h>

int main()
{
   char str[80];

   sprintf(str, "Pi 的值 = %f", M_PI);
   puts(str);
   
   return(0);
}

6.fprintf()

 C 标准库 - &lt;stdio.h&gt; C 标准库 - <stdio.h>

描述

C 库函数 int fprintf(FILE stream, const char format, ...) 发送格式化输出到流 stream 中。

声明

下面是 fprintf() 函数的声明。

int fprintf(FILE stream, const char format, ...)

参数

  • stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
  • format -- 这是 C 字符串,包含了要被写入到流 stream 中的文本。它可以包含嵌入的 format 标签,format 标签可被随后的附加参数中指定的值替换,并按需求进行格式化。format 标签属性是 %flags.precisionspecifier,具体讲解如下:
specifier(说明符)输出
c字符
d 或 i有符号十进制整数
e使用 e 字符的科学科学记数法(尾数和指数)
E使用 E 字符的科学科学记数法(尾数和指数)
f十进制浮点数
g自动选择 %e 或 %f 中合适的表示法
G自动选择 %E 或 %f 中合适的表示法
o有符号八进制
s字符的字符串
u无符号十进制整数
x无符号十六进制整数
X无符号十六进制整数(大写字母)
p指针地址
n无输出
%字符
flags(标识)描述
-在给定的字段宽度内左对齐,默认是右对齐(参见 width 子说明符)。
+强制在结果之前显示加号或减号(+ 或 -),即正数前面会显示 + 号。默认情况下,只有负数前面会显示一个 - 号。
(space)如果没有写入任何符号,则在该值前面插入一个空格。
#与 o、x 或 X 说明符一起使用时,非零值前面会分别显示 0、0x 或 0X。
与 e、E 和 f 一起使用时,会强制输出包含一个小数点,即使后边没有数字时也会显示小数点。默认情况下,如果后边没有数字时候,不会显示显示小数点。
与 g 或 G 一起使用时,结果与使用 e 或 E 时相同,但是尾部的零不会被移除。
0在指定填充 padding 的数字左边放置零(0),而不是空格(参见 width 子说明符)。
width(宽度)描述
(number)要输出的字符的最小数目。如果输出的值短于该数,结果会用空格填充。如果输出的值长于该数,结果不会被截断。
*宽度在 format 字符串中未指定,但是会作为附加整数值参数放置于要被格式化的参数之前。
.precision(精度)描述
.number对于整数说明符(d、i、o、u、x、X):precision 指定了要写入的数字的最小位数。如果写入的值短于该数,结果会用前导零来填充。如果写入的值长于该数,结果不会被截断。精度为 0 意味着不写入任何字符。
对于 e、E 和 f 说明符:要在小数点后输出的小数位数。
对于 g 和 G 说明符:要输出的最大有效位数。
对于 s: 要输出的最大字符数。默认情况下,所有字符都会被输出,直到遇到末尾的空字符。
对于 c 类型:没有任何影响。
当未指定任何精度时,默认为 1。如果指定时不带有一个显式值,则假定为 0。
.*精度在 format 字符串中未指定,但是会作为附加整数值参数放置于要被格式化的参数之前。
length(长度)描述
h参数被解释为短整型或无符号短整型(仅适用于整数说明符:i、d、o、u、x 和 X)。
l参数被解释为长整型或无符号长整型,适用于整数说明符(i、d、o、u、x 和 X)及说明符 c(表示一个宽字符)和 s(表示宽字符字符串)。
L参数被解释为长双精度型(仅适用于浮点数说明符:e、E、f、g 和 G)。
  • 附加参数 -- 根据不同的 format 字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了 format 参数中指定的每个 % 标签。参数的个数应与 % 标签的个数相同。

返回值

如果成功,则返回写入的字符总数,否则返回一个负数。

实例

下面的实例演示了 fprintf() 函数的用法。

include <stdio.h>

include <stdlib.h>

int main()
{
   FILE * fp;

   fp = fopen ("file.txt", "w+");
   fprintf(fp, "%s %s %s %d", "We", "are", "in", 2014);
   
   fclose(fp);
   
   return(0);
}

让我们编译并运行上面的程序,这将创建文件 file.txt,它的内容如下:
We are in 2014

7.fputc()

 C 标准库 - &lt;stdio.h&gt; C 标准库 - <stdio.h>

描述

C 库函数 int fputc(int char, FILE *stream) 把参数 char 指定的字符(一个无符号字符)写入到指定的流 stream 中,并把位置标识符往前移动。

声明

下面是 fputc() 函数的声明。

int fputc(int char, FILE *stream)

参数

  • char -- 这是要被写入的字符。该字符以其对应的 int 值进行传递。
  • stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符的流。

返回值

如果没有发生错误,则返回被写入的字符。如果发生错误,则返回 EOF,并设置错误标识符。

实例

下面的实例演示了 fputc() 函数的用法。

include <stdio.h>

int main ()
{
FILE *fp;
int ch;

fp = fopen("file.txt", "w+");
for( ch = 33 ; ch <= 100; ch++ )
{

  fputc(ch, fp);

}
fclose(fp);

return(0);
}

让我们编译并运行上面的程序,这将在当前目录中创建文件 file.txt,它的内容如下:

!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcd

8.fseek()

 C 标准库 - &lt;stdio.h&gt; C 标准库 - <stdio.h>

描述

C 库函数 int fseek(FILE *stream, long int offset, int whence) 设置流 stream 的文件位置为给定的偏移 offset,参数 offset 意味着从给定的 whence 位置查找的字节数。

声明

下面是 fseek() 函数的声明。

int fseek(FILE *stream, long int offset, int whence)

参数

  • stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
  • offset -- 这是相对 whence 的偏移量,以字节为单位。
  • whence -- 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:
常量描述
SEEK_SET文件的开头
SEEK_CUR文件指针的当前位置
SEEK_END文件的末尾

返回值

如果成功,则该函数返回零,否则返回非零值。

实例

下面的实例演示了 fseek() 函数的用法。

include <stdio.h>

int main ()
{
FILE *fp;

fp = fopen("file.txt","w+");
fputs("This is runoob.com", fp);

fseek( fp, 7, SEEK_SET );
fputs(" C Programming Langauge", fp);
fclose(fp);

return(0);
}

让我们编译并运行上面的程序,这将创建文件 file.txt,它的内容如下。最初程序创建文件和写入 _This is runoob.com_,但是之后我们在第七个位置重置了写指针,并使用 puts() 语句来重写文件,内容如下:

This is C Programming Langauge

10.remove()

 C 标准库 - &lt;stdio.h&gt; C 标准库 - <stdio.h>

描述

C 库函数 int remove(const char *filename) 删除给定的文件名 filename,以便它不再被访问。

声明

下面是 remove() 函数的声明。

int remove(const char *filename)

参数

  • filename -- 这是 C 字符串,包含了要被删除的文件名称。

返回值

如果成功,则返回零。如果错误,则返回 -1,并设置 errno。

实例

下面的实例演示了 remove() 函数的用法。

include <stdio.h>

include <string.h>

int main ()
{
int ret;
FILE *fp;
char filename[] = "file.txt";

fp = fopen(filename, "w");

fprintf(fp, "%s", "这里是 runoob.com");
fclose(fp);

ret = remove(filename);

if(ret == 0)
{

  printf("文件删除成功");

}
else
{

  printf("错误:不能删除该文件");

}

return(0);
}

假设我们有一个文本文件 file.txt,它的内容如下。我们将使用上面的程序来删除该文件。让我们编译并运行上面的程序,这将生成下面的消息,且文件被永久删除。

文件删除成功

11.memcpy()

 C 标准库 - &lt;string.h&gt; C 标准库 - <string.h>

描述

C 库函数 void memcpy(void str1, const void *str2, size_t n) 从存储区 str2 复制 n 个字节到存储区 str1

声明

下面是 memcpy() 函数的声明。

void memcpy(void str1, const void *str2, size_t n)

参数

  • str1 -- 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
  • str2 -- 指向要复制的数据源,类型强制转换为 void* 指针。
  • n -- 要被复制的字节数。

返回值

该函数返回一个指向目标存储区 str1 的指针。

实例

下面的实例演示了 memcpy() 函数的用法。

实例

// 将字符串复制到数组 dest 中

include <stdio.h>

include <string.h>

int main ()
{
const char src[50] = "http://www.runoob.com";
char dest[50];

memcpy(dest, src, strlen(src)+1);
printf("dest = %s\n", dest);

return(0);
}

让我们编译并运行上面的程序,这将产生以下结果:

dest = http://www.runoob.com

将 s 中第 11 个字符开始的 6个连续字符复制到 d 中:

实例

include <stdio.h>

include<string.h>

int main()

{
char *s="http://www.runoob.com";
char d[20];
memcpy(d, s+11, 6);// 从第 11 个字符(r)开始复制,连续复制 6 个字符(runoob)
// 或者 memcpy(d, s+11sizeof(char), 6sizeof(char));
d[6]='\0';
printf("%s", d);
return 0;
}

让我们编译并运行上面的程序,这将产生以下结果:

runoob

覆盖原有部分数据:

实例

include<stdio.h>

include<string.h>

int main(void)
{
char src[] = "*";
char dest[] = "abcdefg";
printf("使用 memcpy 前: %s\n", dest);
memcpy(dest, src, strlen(src));
printf("使用 memcpy 后: %s\n", dest);
return 0;
}

让我们编译并运行上面的程序,这将产生以下结果:

使用 memcpy 前: abcdefg
使用 memcpy 后: *defg

12.strncmp()

 C 标准库 - &lt;string.h&gt; C 标准库 - <string.h>

描述

strncmp() 是一个标准库函数,用于比较两个字符串的前 n 个字符是否相等。

strncmp() 函数通常用于比较两个字符串,以确定它们是否相等或哪个字符串在字典顺序上更小。

C 库函数 int strncmp(const char str1, const char str2, size_t n) 把 str1 和 str2 进行比较,最多比较前 n 个字符。

声明

下面是 strncmp() 函数的声明。

int strncmp(const char str1, const char str2, size_t n)

参数

  • str1 -- 要进行比较的第一个字符串。
  • str2 -- 要进行比较的第二个字符串。
  • n -- 要比较的最大字符数。

返回值

该函数返回值如下:

  • 如果返回值 < 0,则表示 str1 小于 str2。
  • 如果返回值 > 0,则表示 str1 大于 str2。
  • 如果返回值 = 0,则表示 str1 等于 str2。

实例

下面的实例演示了 strncmp() 函数的用法。

include <stdio.h>

include <string.h>

int main ()
{
   char str1[15];
   char str2[15];
   int ret;

   strcpy(str1, "abcdef");
   strcpy(str2, "ABCDEF");

   ret = strncmp(str1, str2, 4);

   if(ret < 0)
   {
      printf("str1 小于 str2");
   }
   else if(ret > 0)
   {
      printf("str2 小于 str1");
   }
   else
   {
      printf("str1 等于 str2");
   }
   
   return(0);
}

让我们编译并运行上面的程序,这将产生以下结果:

str2 小于 str1

13.fgets()

描述

C 库函数 char fgets(char str, int n, FILE *stream) 从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。

声明

下面是 fgets() 函数的声明。

char fgets(char str, int n, FILE *stream)

参数

  • str -- 这是指向一个字符数组的指针,该数组存储了要读取的字符串。
  • n -- 这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
  • stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。

返回值

如果成功,该函数返回相同的 str 参数。如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。

如果发生错误,返回一个空指针。

实例

下面的实例演示了 fgets() 函数的用法。

include <stdio.h>

int main()
{
FILE *fp;
char str[60];

/ 打开用于读取的文件 /
fp = fopen("file.txt" , "r");
if(fp == NULL) {

  perror("打开文件时发生错误");
  return(-1);

}
if( fgets (str, 60, fp)!=NULL ) {

  /* 向标准输出 stdout 写入内容 */
  puts(str);

}
fclose(fp);

return(0);
}

假设我们有一个文本文件 file.txt,它的内容如下。文件将作为实例中的输入:

We are in 2014

让我们编译并运行上面的程序,这将产生以下结果:

We are in 2014

14.fflush()

描述

C 库函数 int fflush(FILE *stream) 刷新流 stream 的输出缓冲区。
**
fflush(stdio):清空输入缓冲区**

fflush(stdout):清空输出缓冲区

声明

下面是 fflush() 函数的声明。

int fflush(FILE *stream)

参数

  • stream -- 这是指向 FILE 对象的指针,该 FILE 对象指定了一个缓冲流。

返回值

如果成功,该函数返回零值。如果发生错误,则返回 EOF,且设置错误标识符(即 feof)。

实例

下面的实例演示了 fflush() 函数的用法。

#include <stdio.h>
#include <string.h>
 
int main()
{
 
   char buff[1024];
 
   memset( buff, '\0', sizeof( buff ));
 
   fprintf(stdout, "启用全缓冲\n");
   setvbuf(stdout, buff, _IOFBF, 1024);
 
   fprintf(stdout, "这里是 runoob.com\n");
   fprintf(stdout, "该输出将保存到 buff\n");
   fflush( stdout );
 
   fprintf(stdout, "这将在编程时出现\n");
   fprintf(stdout, "最后休眠五秒钟\n");
 
   sleep(5);
 
   return(0);
}

我们编译并运行上面的程序,这将产生以下结果。在这里,程序把缓冲输出保存到 buff,直到首次调用 fflush() 为止,然后开始缓冲输出,最后休眠 5 秒钟。它会在程序结束之前,发送剩余的输出到 STDOUT。

启用全缓冲
这里是 runoob.com
该输出将保存到 buff
这将在编程时出现
最后休眠五秒钟

15.read()和pread()

(1)read()

read函数的原型为:

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

该函数用于从指定文件描述符 fd对应的文件中读取数据,并将读取到的数据存储到用户空间的缓冲区 buf 中,其中参数 count 表示要读取的字节数
读取成功后,返回实际读取到的字节数;如果发生错误则返回-1。

(2)pread

pread函数的原型为:

#include <unistd.h>

ssize_t pread(int fd, void *buf, size_t count, off_t offset);

与read函数不同的是,pread函数可以从指定文件描述符 fd对应的文件中,从偏移量 offset 处开始读取数据,并将读取到的数据存储到用户空间的缓冲区 buf中,其中参数 count 表示要读取的字节数。
读取成功后,返回实际读取到的字节数;如果发生错误则返回-1。

(3)read函数和pread函数的区别

read函数和pread函数都是用于从文件中读取数据的函数。

其中,read函数是标准I/O库提供的函数,pread函数是POSIX标准中定义的函数。它们之间的主要区别有:

  • 调用方式不同:read函数的调用方式为 int read(int fd, void buf, size_t count),pread函数的调用方式为 ssize_t pread(int fd, void buf, size_t count, off_t offset)。
  • 文件偏移量不同:使用read函数读取文件时,每次读取后,文件指针会自动向后移动相应的字节数;而使用pread函数读取文件时,文件指针不会改变,仍然指向之前的位置。这就意味着,在读取文件时,pread保证了数据的原子性和一致性。
  • 处理并发访问时的差异:在多线程或多进程并发访问同一个文件时,使用read函数可能会出现竞态条件(race condition),可能会导致数据不一致性。而pread函数由于保证了文件指针的不变性,可以安全地在多个线程之间共享。

总的来说,如果需要在多线程或多进程环境下读取文件,建议使用pread函数。否则,选择哪种函数需要根据具体情况进行判断。

16. setbuf()

描述

C 库函数 void setbuf(FILE stream, char buffer) 定义流 stream 应如何缓冲。该函数应在与流 stream 相关的文件被打开时,且还未发生任何输入或输出操作之前被调用一次。

声明

下面是 setbuf() 函数的声明。

void setbuf(FILE *stream, char *buffer)

参数

  • stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了一个打开的流。
  • buffer -- 这是分配给用户的缓冲,它的长度至少为 BUFSIZ 字节,BUFSIZ 是一个宏常量,表示数组的长度。

返回值

该函数不返回任何值。

实例

下面的实例演示了 setbuf() 函数的用法。


#include <stdio.h>

int main()
{
   char buf[BUFSIZ];

   setbuf(stdout, buf);
   puts("This is runoob");

   fflush(stdout);
   return(0);
}

让我们编译并运行上面的程序,这将产生以下结果。在这里,程序在即将输出的时候,发送输出到 STDOUT,否则它将缓冲输出。您也可以使用 fflush() 函数来 to 刷新输出。

This is runoob

17.srand()和rand()

(1)srand()

描述

C 库函数 void srand(unsigned int seed) 播种由函数 rand 使用的随机数发生器。

声明

下面是 srand() 函数的声明。

void srand(unsigned int seed)

参数

  • seed -- 这是一个整型值,用于伪随机数生成算法播种。

返回值

该函数不返回任何值。

实例

下面的实例演示了 srand() 函数的用法。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
 
int main()
{
   int i, n;
   time_t t;
   
   n = 5;
   
   /* 初始化随机数发生器 */
   srand((unsigned) time(&t));
 
   /* 输出 0 到 50 之间的 5 个随机数 */
   for( i = 0 ; i < n ; i++ ) {
      printf("%d\n", rand() % 50);
   }
   
  return(0);
}

让我们编译并运行上面的程序,这将产生以下结果:

38
45
29
29
47

(2)rand()

 C 标准库 - &lt;stdlib.h&gt; C 标准库 - <stdlib.h>

描述

C 库函数 int rand(void) 返回一个范围在 0 到 _RAND_MAX_ 之间的伪随机数。

RAND_MAX 是一个常量,它的默认值在不同的实现中会有所不同,但是值至少是 32767。

声明

下面是 rand() 函数的声明。

int rand(void)

参数

  • NA

返回值

该函数返回一个范围在 0 到 RAND_MAX 之间的整数值。

实例

下面的实例演示了 rand() 函数的用法。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
 
int main()
{
   int i, n;
   time_t t;
   
   n = 5;
   
   /* 初始化随机数发生器 */
   srand((unsigned) time(&t));
 
   /* 输出 0 到 49 之间的 5 个随机数 */
   for( i = 0 ; i < n ; i++ ) {
      printf("%d\n", rand() % 50);
   }
   
  return(0);
}

让我们编译并运行上面的程序,这将产生以下结果:

38
45
29
29
47

18.mmap()

说明

mmap()用来将某个文件内容映射到内存中,对该内存区域的存取即是直接对该文件内容的读写。

#include <sys/mman.h>

void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);

参数

start: 通常设为NULL, 系统会自动选择内存地址
length: 文件中要进行映射到内存的数据长度
prot: 映射区域保护方式,组合如下:

选项含义
PROT_EXEC映射区域可被执行
PROT_READ映射区域可被读取
PROT_WRITE映射区域可被写入
PROT_NONE映射区域不能存取

flags: 特性设置,如下:

选项含义
MAP_FIXED如果参数 start 所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标
MAP_SHARED对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享
MAP_PRIVATE对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容
MAP_ANONYMOUS建立匿名映射。此时会忽略参数 fd,不涉及文件,而且映射区域无法和其他进程共享
MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝
MAP_LOCKED将映射区域锁定住,这表示该区域不会被置换(swap)

在调用 mmap()时必须要指定 MAP_SHARED 或 MAP_PRIVATE。
参数 fd 为 open()返回的文件描述词,代表欲映射到内存的文件。参
数 offset 为文件映射的偏移量,通常设置为 0,代表从文件最前方
开始对应,offset 必须是分页大小的整数倍

  • 返回值

若映射成功则返回映射区的内存起始地址,否则返回 MAP_FAILED(-1),错误原因存于 errno 中

errno含义
EBADF参数 fd 不是有效的文件描述符
EACCES存取权限有误。如果是 MAP_PRIVATE 情况下文件必须可读,使用 MAP_SHARED 则要有 PROT_WRITE 以及该文件要能写入。
EINVAL参数 start、length 或 offset 有一个不合法。
EAGAIN文件被锁住,或是有太多内存被锁住。
ENOMEM内存不足。
  • 相关函数

munmap,open

例子

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

int main()
{
    int fd;
    struct stat st;
    void *p = NULL;

    fd = open("abc.txt", O_RDONLY);
    if(fd < 0){
        printf("open file failed\n");
        return -1;
    }

    fstat(fd, &st);

    p = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if(p == MAP_FAILED)
        return -2;

    printf("map file content: %s\n", (char *)p);

    munmap(p, st.st_size);
    return 0;
}

执行结果

$ echo "hello arvik" > abc.txt
$ cat abc.txt 
hello arvik
$ ./test 
map file content: hello arvik

原理

1. mmap 基础概念

mmap 即 memory map,也就是内存映射。

mmap 是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用 read、write 等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。如下图所示:
![[逆向笔记/mdpic/Pasted image 20231217110803.png]]
mmap 具有如下的特点:

1.mmap 向应用程序提供的内存访问接口是内存地址连续的,但是对应的磁盘文件的 block 可以不是地址连续的;
2.mmap 提供的内存空间是虚拟空间(虚拟内存),而不是物理空间(物理内存),因此完全可以分配远远大于物理内存大小的虚拟空间(例如 16G 内存主机分配 1000G 的 mmap 内存空间);
3.mmap 负责映射文件逻辑上一段连续的数据(物理上可以不连续存储)映射为连续内存,而这里的文件可以是磁盘文件、驱动假造出的文件(例如 DMA 技术)以及设备;
4.mmap 由操作系统负责管理,对同一个文件地址的映射将被所有线程共享,操作系统确保线程安全以及线程可见性;

mmap 的设计很有启发性。基于磁盘的读写单位是 block(一般大小为 4KB),而基于内存的读写单位是地址(虽然内存的管理与分配单位是 4KB)。换言之,CPU 进行一次磁盘读写操作涉及的数据量至少是 4KB,但是进行一次内存操作涉及的数据量是基于地址的,也就是通常的 64bit(64 位操作系统)。mmap 下进程可以采用指针的方式进行读写操作,这是值得注意的。

2.mmap的I/O模型

![[逆向笔记/mdpic/Pasted image 20231217110906.png]]

mmap 技术有如下特点:

1.利用 DMA 技术来取代 CPU 来在内存与其他组件之间的数据拷贝,例如从磁盘到内存,从内存到网卡;
2.用户空间的 mmap file 使用虚拟内存,实际上并不占据物理内存,只有在内核空间的 kernel buffer cache 才占据实际的物理内存;
2.`mmap()` 函数需要配合 `write() `系统调动进行配合操作,这与 `sendfile()` 函数有所不同,后者一次性代替了 `read()` 以及 `write()`;因此 mmap 也至少需要 4 次上下文切换;
4.mmap 仅仅能够避免内核空间到用户空间的全程 CPU 负责的数据拷贝,但是内核空间内部还是需要全程 CPU 负责的数据拷贝;

利用 mmap() 替换 read(),配合 write() 调用的整个流程如下:

1.用户进程调用 `mmap()`,从用户态陷入内核态,将内核缓冲区映射到用户缓存区;
2.DMA 控制器将数据从硬盘拷贝到内核缓冲区(可见其使用了 Page Cache 机制);
3.`mmap()` 返回,上下文从内核态切换回用户态;
4.用户进程调用` write()`,尝试把文件数据写到内核里的套接字缓冲区,再次陷入内核态;
5.CPU 将内核缓冲区中的数据拷贝到的套接字缓冲区;
5.DMA 控制器将数据从套接字缓冲区拷贝到网卡完成数据传输;
6`.write()` 返回,上下文从内核态切换回用户态。

3. mmap 的优势

(1)简化用户进程编程

在用户空间看来,通过 mmap 机制以后,磁盘上的文件仿佛直接就在内存中,把访问磁盘文件简化为按地址访问内存。这样一来,应用程序自然不需要使用文件系统的 write(写入)、read(读取)、fsync(同步)等系统调用,因为现在只要面向内存的虚拟空间进行开发。

但是,这并不意味着我们不再需要进行这些系统调用,而是说这些系统调用由操作系统在 mmap 机制的内部封装好了。

1)基于缺页异常的懒加载

出于节约物理内存以及 mmap 方法快速返回的目的,mmap 映射采用懒加载机制。具体来说,通过 mmap 申请 1000G 内存可能仅仅占用了 100MB 的虚拟内存空间,甚至没有分配实际的物理内存空间。当你访问相关内存地址时,才会进行真正的 write、read 等系统调用。CPU 会通过陷入缺页异常的方式来将磁盘上的数据加载到物理内存中,此时才会发生真正的物理内存分配。

2)数据一致性由 OS 确保

当发生数据修改时,内存出现脏页,与磁盘文件出现不一致。mmap 机制下由操作系统自动完成内存数据落盘(脏页回刷),用户进程通常并不需要手动管理数据落盘。

(2)读写效率提高:避免内核空间到用户空间的数据拷贝

简而言之,mmap 被认为快的原因是因为建立了页到用户进程的虚地址空间映射,以读取文件为例,避免了页从内核空间拷贝到用户空间。

(3)避免只读操作时的 swap 操作

虚拟内存带来了种种好处,但是一个最大的问题在于所有进程的虚拟内存大小总和可能大于物理内存总大小,因此当操作系统物理内存不够用时,就会把一部分内存 swap 到磁盘上。

在 mmap 下,如果虚拟空间没有发生写操作,那么由于通过 mmap 操作得到的内存数据完全可以通过再次调用 mmap 操作映射文件得到。但是,通过其他方式分配的内存,在没有发生写操作的情况下,操作系统并不知道如何简单地从现有文件中(除非其重新执行一遍应用程序,但是代价很大)恢复内存数据,因此必须将内存 swap 到磁盘上。

(4)节约内存

由于用户空间与内核空间实际上共用同一份数据,因此在大文件场景下在实际物理内存占用上有优势。

4. mmap 不是银弹

mmap 不是银弹,这意味着 mmap 也有其缺陷,在相关场景下的性能存在缺陷:

1.由于 mmap 使用时必须实现指定好内存映射的大小,因此 mmap 并不适合变长文件;
2.如果更新文件的操作很多,mmap 避免两态拷贝的优势就被摊还,最终还是落在了大量的脏页回写及由此引发的随机 I/O 上,所以在随机写很多的情况下,mmap 方式在效率上不一定会比带缓冲区的一般写快;
3.读/写小文件(例如 16K 以下的文件),mmap 与通过 read 系统调用相比有着更高的开销与延迟;同时 mmap 的刷盘由系统全权控制,但是在小数据量的情况下由应用本身手动控制更好;
4..mmap 受限于操作系统内存大小:例如在 32-bits 的操作系统上,虚拟内存总大小也就 2GB,但由于 mmap 必须要在内存中找到一块连续的地址块,此时你就无法对 4GB 大小的文件完全进行 mmap,在这种情况下你必须分多块分别进行 mmap,但是此时地址内存地址已经不再连续,使用 mmap 的意义大打折扣,而且引入了额外的复杂性;

(5) mmap 的适用场景

mmap 的适用场景实际上非常受限,在如下场合下可以选择使用 mmap 机制:

    1.多个线程以只读的方式同时访问一个文件,这是因为 mmap 机制下多线程共享了同一物理内存空间,因此节约了内存。案例:多个进程可能依赖于同一个动态链接库,利用 mmap 可以实现内存仅仅加载一份动态链接库,多个进程共享此动态链接库。
    2.mmap 非常适合用于进程间通信,这是因为对同一文件对应的 mmap 分配的物理内存天然多线程共享,并可以依赖于操作系统的同步原语;
    3.mmap 虽然比 sendfile 等机制多了一次 CPU 全程参与的内存拷贝,但是用户空间与内核空间并不需要数据拷贝,因此在正确使用情况下并不比 sendfile 效率差;
C语言 库函数
Theme Jasmine by Kent Liao