C语言Linux环境编程

前言

本文的目的是对学 Pwn 以来接触到的库函数以及 API 做一个总结,同时也是为了学习一下 Linux 环境下的多线程编程以及网络编程这些知识。

内联汇编

内联汇编是将汇编代码直接嵌入 C 函数中,可以在 C 程序中直接使用汇编指令。gcc提供了asm关键字来实现内联汇编。

  • 语法
c 复制代码
asm ( "syscall" );

在 Pwn 出题中经常用到volatile关键字防止汇编代码被优化。

c 复制代码
#include<stdio.h>

char name[0x100];

void gift()
{
    asm volatile ("syscall;\n\t");
}
  
void main()
{
    char buf[0x20];
    puts("name:");
    read(0,name,0x100);
    puts("passwd:");
    read(0,buf,0x40);
}

常用库函数

字符串操作函数

  • strlen

功能:计算字符串的长度(不包括终止符 \0)。

原型:size_t strlen(const char *str);

例:

在 Pwn 中限制输入长度的题目中,经常会看到\x00字节绕过,即在字符串开头放一个\x00字节。这样strlen函数就直接截断到第一个字符。

c 复制代码
char str[]={0,'a',0};
printf("%d\n",strlen(str));//最后的str的长度是0
  • strncpy

功能:复制指定长度的字符串。

原型:char *strncpy(char *dest, const char *src, size_t n);

例:

在正常使用的情况下,strncpy函数并不会给复制到目标字符数组的字符串后面添加\x00

并且strncpy函数并不会检查源字符串的实际长度,如果你给定的复制长度n大于源字符串的长度,strncpy会将源字符串用\x00填充到指定长度,这可能会导致栈溢出。

c 复制代码
char dest[0x10];
strncpy(dest, "Hello", 0x20);
  • strcpy

功能:复制一个字符串到另一个字符串,遇到\x00停止。

原型:char *strcpy(char *dest, const char *src);

例:

strcpy函数复制源字符串到一个新的字符数组,但是strcpy函数并不会检查源字符串长度,直到遇到\x00截断它才会停止复制。同时它也不会检查目标数组的长度,如果源字符串的长度远超于目标数组的长度则会导致栈溢出。

c 复制代码
char dest[6];
strcpy(dest, "Hello");
  • strcat

功能:将源字符串连接到目标字符串的末尾。

原型:char *strcat(char *dest, const char *src);

例:

同样strcat函数在复制源字符串时也是到\x00终止,而且也不会检查目标字符数组的大小,因此如果目标数组不足以容纳拼接后的结果,strcat将继续写入超出目标数组的数据,导致缓冲区溢出。

c 复制代码
char dest[8] = "Hello";
strcat(dest, " World");
  • strstr

功能:在一个字符串中查找另一个字符串的首次出现位置。

原型:char *strstr(const char *haystack, const char *needle);

例:

c 复制代码
char *pos = strstr("Hello World", "World");  // pos 指向 "World"
  • strtok

功能:分割字符串,返回分割后的每个子串。

原型:char *strtok(char *str, const char *delim);

例:

c 复制代码
char str[] = "Hello, world!";
char *token = strtok(str, ", ");  // 以,分割字符串
  • sprintf

功能:将格式化数据写入字符串。

原型:int sprintf(char *str, const char *format, ...);

例:

sprintf不会检查目标缓冲区的大小,因此如果格式化后的字符串超过了目标缓冲区的容量,sprintf会继续写入超出缓冲区的数据,导致缓冲区溢出。在sprintf中源数据的长度取决于\x00字节。

c 复制代码
char buffer[50];
sprintf(buffer, "Hello %d", 42);  //buffer=Hello 42
  • snprintf

功能:snprintfsprintf的安全版本,防止缓冲区溢出。

原型:int snprintf(char *str, size_t size, const char *format, ...);

例:

snprintf通过size参数限制写入的数据长度。

c 复制代码
char buffer[50];
snprintf(buffer, sizeof(buffer), "Hello %d", 42);  // 防止溢出
  • strchr

功能:strchr查找字符串中首次出现的字符位置,

原型:char *strchr(const char *str, int c);

例:

c 复制代码
char *pos = strchr("Hello", 'l');  // 找到第一个 'l'
  • strrchr

功能:strrchr查找最后一次出现的字符位置。

原型:char *strrchr(const char *str, int c);

例:

c 复制代码
char *rpos = strrchr("Hello", 'l');  // 找到最后一个 'l'
  • chown

功能:chown 用于更改文件的所有者和组。

原型: int chown(const char *path, uid_t owner, gid_t group);

例:

c 复制代码
chown("file.txt", 0, 0);  // 修改文件的所有者和组为root
  • chmod

功能:chmod用于更改文件的权限。

原型:int chmod(const char *path, mode_t mode);

例:

c 复制代码
chmod("file.txt", S_IRUSR | S_IWUSR);  // 设置文件为可读写
  • dlopen

功能:用于动态加载共享库,

原型:void *dlopen(const char *filename, int flag);

例:

c 复制代码
void *handle = dlopen("libm.so", RTLD_LAZY);  // 加载共享库
  • dlsym

功能:用于获取库中的符号(函数地址)。

原型:void *dlsym(void *handle, const char *symbol);

例:

c 复制代码
double (*cos_func)(double) = dlsym(handle, "cos");  // 获取 cos 函数地址
  • ptrace

功能:进程跟踪,用于调试程序,允许一个进程控制另一个进程。

原型:long ptrace(enum __ptrace_request request,

例:

让当前进程成为其父进程的目标进程,允许父进程使用ptrace跟踪它。通常用于子进程初始化时。

c 复制代码
ptrace(PTRACE_TRACEME, 0, NULL, NULL);

内存操作函数

  • memset

功能:将一块内存区域的内容设置为指定的值。

原型:void *memset(void *str, int c, size_t n);

例:

c 复制代码
char buffer[10];
memset(buffer, 0, sizeof(buffer));  // 将 buffer 填充为 0
  • memmove

功能:在内存中复制指定字节数的数据,允许内存重叠。

原型:void *memmove(void *dest, const void *src, size_t n);

例:

c 复制代码
char src[] = "Hello";
memmove(src + 2, src, 5);  // 将 "Hello" 移动为 "HeHello"
  • memcpy

功能:在内存中复制指定字节数的数据,不允许内存重叠。

原型:void *memcpy(void *dest, const void *src, size_t n);

例:

c 复制代码
char dest[6];
memcpy(dest, "Hello", 5);  // 复制 5 个字节
  • memcmp

功能:比较两块内存的内容。

原型:int memcmp(const void *str1, const void *str2, size_t n);

例:

c 复制代码
int result = memcmp("abc", "abd", 3);  // result < 0
  • atoi

功能:用于将字符串转换为整数

原型:int atoi(const char *str);

例:

c 复制代码
atoi("10");//转换为整数10

字符串比较函数

  • strcmp

功能:比较两个字符串的大小(逐字符),遇到\x00字符终止。

原型:int strcmp(const char *str1, const char *str2);

例:

c 复制代码
int result = strcmp("abc", "abd");  // result < 0
  • strncmp

功能:比较两个字符串的前 n 个字符,遇到\x00字符终止。

原型:int strncmp(const char *str1, const char *str2, size_t n);

例:

c 复制代码
int result = strncmp("abc", "abd", 3);  // result < 0
  • strcasecmp

功能:比较两个字符串,忽略大小写。逐个字符比较直到遇到\x00结束符。

原型:int strcasecmp(const char *str1, const char *str2);

例:

c 复制代码
strcasecmp("hello", "Hello");
  • strcspn

功能:在字符串中查找第一个包含指定字符集合中的字符的字符。

原型:size_t strcspn(const char *str1, const char *str2);

例:

c 复制代码
str="hello,world!"
strcspn(str,",!")

输入输出函数

  • getchar

功能:从标准输入读取一个字符。

原型:int getchar(void);

例:

c 复制代码
char c = getchar();  // 等待用户输入一个字符
  • srand

功能:设置rand生成伪随机数的种子。

原型:void srand(unsigned int seed);

例:

c 复制代码
srand(123);  // 设置随机数种子为 123
  • rand

功能:生成一个伪随机数。

原型:int rand(void);

例:

c 复制代码
int random_number = rand();  // 返回一个随机数
  • scanf

功能:从标准输入读取格式化的输入。

原型:int scanf(const char *format, ...);

c 复制代码
int x;
scanf("%d", &x);  // 输入一个整数
  • printf

功能:向标准输出打印格式化的文本。

原型:int printf(const char *format, ...);

例:

c 复制代码
printf("Hello, %d\n", 42);  // 输出 "Hello, 42"
  • puts

功能:输出一个字符串并换行。

原型:int puts(const char *str);

例:

c 复制代码
puts("Hello");  // 输出 "Hello" 并换行
  • fgets

功能:从文件流读取一行文本。

原型:char *fgets(char *str, int n, FILE *stream);

例:

c 复制代码
char buffer[100];
fgets(buffer, 100, stdin);  // 从标准输入读取一行
  • fputs

功能:将字符串写入文件流。

原型:int fputs(const char *str, FILE *stream);

例:

c 复制代码
fputs("Hello", stdout);  // 将 "Hello" 输出到标准输出
  • fopen

功能:打开一个文件。

原型:FILE *fopen(const char *filename, const char *mode);

例:

c 复制代码
FILE *file = fopen("example.txt", "r");  // 打开文件
  • fprintf

功能:向文件写入格式化文本。

原型:int fprintf(FILE *stream, const char *format, ...);

例:

c 复制代码
FILE *file = fopen("output.txt", "w");
fprintf(file, "Hello %d", 42);  // 向文件写入
fclose(file);
  • sscanf

功能:从字符串中读取格式化的输入。

原型:int sscanf(const char *str, const char *format, ...);

例:

c 复制代码
int x;
sscanf("42", "%d", &x);  // 从字符串中提取整数
  • fclose

功能:关闭文件。

原型:int fclose(FILE *stream);

c 复制代码
fclose(file);  // 关闭文件流

程序控制函数

  • system

功能:执行一个系统命令。

原型:int system(const char *command);

例:

c 复制代码
system("ls");  // 在Linux中列出当前目录
  • setbuf

功能:为给定的文件流设置缓冲区。

原型:void setbuf(FILE *stream, char *buffer);

参数:

  • stream:需要设置缓冲区的文件流;
  • buffer:自定义的缓冲区,如果为NULL表示无缓冲。

例:

通常在 Pwn 中用于给输入输出设置缓冲区。

c 复制代码
setbuf(stdin,0);  //输入无缓冲
setbuf(stdout,0); //输出无缓冲
setbuf(stderr,0); //错误无缓冲
  • setvbuf

功能:为给定的文件流设置缓冲区,并指定缓冲模式。

原型:int setvbuf(FILE *stream, char *buffer, int mode, size_t size);

参数:

  • stream:需要设置缓冲区的文件流;
  • buffer:自定义的缓冲区,如果为NULL,表示不提供自定义缓冲区,系统将自动分配缓冲区。
  • mode:缓冲模式
    • 全缓冲_IOFBF(值0):数据会先写入缓冲区,直到缓冲区满了才会写入文件(或设备)。
    • 行缓存_IOLBF(值1):数据会在遇到换行符(\n)时刷新缓冲区。
    • 无缓冲_IONBF(无缓冲)):没有缓冲区,每次 I/O 操作都会立即执行,不使用缓冲区。
  • size:缓冲区大小,如果为NULL,表示不指定大小。

例:

在 Pwn 中比较常见,一般用于设置输入输出缓冲等。如果设置为全缓冲可能导致我们无法直接泄露libc地址,而必须把缓冲区刷新之后才会输出到标准输出。

c 复制代码
setvbuf(stdin, 0, 2, 0);  //设置输入无缓冲
setvbuf(stdout, 0, 2, 0); //设置输出无缓冲
setvbuf(stderr, 0, 2, 0); //设置错误无缓冲
  • putenv

功能:用于在运行时设置环境变量。

原型:int putenv(char *string);

例:

c 复制代码
putenv("MY_VAR=hello");
  • setenv

功能:用于设置环境变量,且可以在运行时动态修改环境变量。

原型:int setenv(const char *name, const char *value, int overwrite);

name:环境变量的名称(一个字符串)。这是你要设置的环境变量的名字。

value:环境变量的值(一个字符串)。这是你想赋予该环境变量的新值。

overwrite:一个整数值,用来指示是否允许覆盖已存在的环境变量。如果为 1 则覆盖,如果为 0 则不覆盖。

例:

c 复制代码
setenv("MY_VAR", "Hello, World!", 1);
  • unsetenv

功能:删除指定的环境变量。

原型:int unsetenv(const char *name);

例:

c 复制代码
unsetenv("MY_VAR");
  • exit

功能:终止程序的执行。

原型:void exit(int status);

例:

c 复制代码
exit(0);  // 正常退出程序
exit(1);  // 异常退出程序
  • fflush

功能:刷新指定流的缓冲区,将缓冲区中的数据立即写入文件或输出设备。

原型:int fflush(FILE *stream);

例:

c 复制代码
printf("Hello");
fflush(stdout);  // 确保 "Hello" 立即输出到终端
  • abort

功能:中止程序的执行,生成核心转储。

原型:void abort(void);

例:

c 复制代码
abort();  // 立即终止程序执行

管道

C语言中的管道(Pipe)是用于进程间通信(IPC, Inter-Process Communication)的一种机制。管道提供了一种单向的数据传输通道,允许一个进程将数据发送到另一个进程。管道在操作系统中广泛用于进程间的数据传输,尤其是在Linux/Unix系统上。

管道的基本概念

管道有两种类型:

  1. 匿名管道(Anonymous Pipe) :用于具有亲缘关系(父子进程或者兄弟进程)之间的进程通信。匿名管道通常在创建进程时由fork系统调用自动创建。

  2. 命名管道(Named Pipe,FIFO):具有名字的管道,可以在无亲缘关系的进程之间通信。命名管道在文件系统中有一个名字,可以像普通文件一样进行打开和读写。

管道的核心作用是单向传输。数据从写端写入,通过管道传递到读端。

管道的基本使用

1. 创建管道

在C语言中,可以使用pipe()系统调用来创建匿名管道。该系统调用会返回一个整型数组,数组的第一个元素是管道的读端文件描述符,第二个元素是管道的写端文件描述符。

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    int pipefds[2];  // 存放管道的文件描述符
    char buf[100];

    if (pipe(pipefds) == -1) {  // 创建管道
        perror("pipe");
        exit(1);
    }

    pid_t pid = fork();  // 创建子进程
    if (pid == -1) {
        perror("fork");
        exit(1);
    }

    if (pid == 0) {  // 子进程:读取管道数据
        close(pipefds[1]);  // 关闭写端
        read(pipefds[0], buf, sizeof(buf));
        printf("Child Process received: %s\n", buf);
        close(pipefds[0]);
    } else {  // 父进程:写数据到管道
        close(pipefds[0]);  // 关闭读端
        write(pipefds[1], "Hello from parent", 18);
        close(pipefds[1]);
    }

    return 0;
}
2. 代码解释
  • pipe(pipefds):创建一个管道,并将管道的读写端文件描述符保存在pipefds数组中。pipefds[0]是管道的读端,pipefds[1]是管道的写端。

  • fork():创建一个子进程。在父进程中,子进程的PID为0,在子进程中,父进程的PID为正值。

  • close(pipefds[0]):关闭管道的读端(父进程写,子进程读)。

  • write(pipefds[1], ...):向管道写数据。

  • read(pipefds[0], ...):从管道读取数据。

3. 输出结果
bash 复制代码
Child Process received: Hello from parent
4. 管道中的数据传输
  • 数据从写端传递到读端。当父进程向管道写数据时,子进程从管道的读端读取该数据。

  • 在管道通信中,数据传输是阻塞的。即,如果写端没有数据,读端将阻塞直到有数据可读;如果读端没有准备好,写端则会阻塞直到读端准备好。

5. 使用命名管道(FIFO)

命名管道与匿名管道的主要区别在于,命名管道有一个名字,可以在不同的进程间通过文件路径访问。

使用mkfifo函数创建命名管道,并使用open函数打开该管道。命名管道的基本使用步骤如下:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int main() {
    const char *fifo_path = "/tmp/myfifo";  // 管道的路径
    char buf[100];

    // 创建FIFO(命名管道)
    if (mkfifo(fifo_path, 0666) == -1) {
        perror("mkfifo");
        exit(1);
    }

    pid_t pid = fork();  // 创建子进程
    if (pid == -1) {
        perror("fork");
        exit(1);
    }

    if (pid == 0) {  // 子进程:读取命名管道
        int fd = open(fifo_path, O_RDONLY);
        if (fd == -1) {
            perror("open");
            exit(1);
        }
        read(fd, buf, sizeof(buf));
        printf("Child Process received: %s\n", buf);
        close(fd);
    } else {  // 父进程:写数据到命名管道
        int fd = open(fifo_path, O_WRONLY);
        if (fd == -1) {
            perror("open");
            exit(1);
        }
        write(fd, "Hello from parent to FIFO", 24);
        close(fd);
    }

    return 0;
}

Linux API 函数

  • read

功能:从文件描述符读取数据。

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

例:

c 复制代码
char buffer[128];
read(0, buffer, 128);  // 从标准输入读取数据
  • write

功能:将数据写入文件描述符,通常用于向文件或设备写数据。

原型:ssize_t write(int fd, const void *buf, size_t count);

例:

c 复制代码
write(1, "Hello, world!", 13);  // 输出字符串
  • getpid

功能:返回当前进程的 pid。

原型:pid_t getpid(void);

例:

c 复制代码
pid_t pid = getpid();
  • fork

功能:创建一个新的进程,子进程是父进程的副本。

原型:pid_t fork(void);

例:

c 复制代码
pid_t pid = fork();  // 创建子进程
if (pid == 0) {
    // 子进程代码
} else {
    // 父进程代码
	}
  • waitpid

功能:等待子进程的状态改变

原型:pid_t waitpid(pid_t pid, int *status, int options);

pid:指定要等待的子进程的 PID。如果为 -1,则等待任意子进程。

status:用来保存子进程的退出状态,可以通过WIFEXITEDWEXITSTATUS等宏分析子进程的退出情况。

options:控制行为的选项,通常是 0 或一些位标志,常用选项如WNOHANG如果没有子进程终止,waitpid不会阻塞,直接返回。WUNTRACED返回停止的子进程。

返回值:返回子进程的 PID,如果出错则返回 -1。

例:

c 复制代码
pid_t pid = fork();
pid_t child_pid = waitpid(pid, &status, 0); // 等待子进程结束
  • sysconf

功能:获取系统配置参数,如最大文件描述符数、内存页大小等。

原型:long sysconf(int name);

例:

c 复制代码
long pagesize = sysconf(_SC_PAGESIZE);  // 获取内存页大小
  • signal

功能:设置一个信号的处理函数,用于捕获和处理信号。

原型:sighandler_t signal(int signum, sighandler_t handler);

例:

c 复制代码
signal(SIGINT, handler);  // 捕获 Ctrl+C 信号并调用 handler 函数
  • alarm

功能:设置一个定时器,在指定的秒数后发送 SIGALRM 信号。

原型:unsigned int alarm(unsigned int seconds);

例:

c 复制代码
alarm(5);  // 5秒后发送 SIGALRM 信号
  • prctl

功能:设置一个定时器,在指定的秒数后发送SIGALRM信号。

原型:unsigned int alarm(unsigned int seconds);

例:

c 复制代码
prctl(PR_SET_NAME, "my_process");  // 设置进程名为 "my_process"
  • setuid

功能:设置进程的用户ID。

原型:int setuid(uid_t uid);

例:

c 复制代码
setuid(0);  // 设置用户ID为root
  • setgid

功能:设置进程的组ID。

原型:int setgid(gid_t gid);

例:

c 复制代码
setgid(0);  // 设置组ID为root
  • perror

功能:输出上一条系统调用的错误信息。

原型:void perror(const char *s);

例:

c 复制代码
if (open("file.txt", O_RDONLY) == -1) {
        perror("Error opening file");  // 输出错误信息
    }

exec系列函数

exec系列函数用于执行一个新的程序,替换当前进程的镜像。在 Pwn 中比较常见,可以用于 getshell。

  • execvp()

原型:int execvp(const char *file, char *const argv[]);

file:要执行的程序的路径。如果只给出程序名,execvp会在环境变量PATH指定的路径中查找该程序。

argv:参数数组,第一个元素是程序名,后续的元素是传递给程序的参数。

例:

c 复制代码
execvp("/bin/sh", NULL);
  • execv()

原型:int execv(const char *path, char *const argv[]);

path:可执行文件的完整路径。

argv:参数数组,第一个元素是程序名,后续的元素是传递给程序的参数。

例:

c 复制代码
execv("/bin/sh", NULL);
  • execve()

原型:int execve(const char *pathname, char *const argv[], char *const envp[]);

pathname:要执行的程序的完整路径。

argv:参数数组,第一个元素是程序名,后续的元素是传递给程序的参数。

envp:环境变量数组,包含要传递给新程序的环境变量。

例:

c 复制代码
execve("/bin/sh", NULL, NULL);
  • execl()

原型:int execl(const char *path, const char *arg, ... /* (char *) NULL */);

path:可执行文件的完整路径。

arg:程序的第一个参数,通常是程序的名称。

后续的参数依次为程序需要传递的各个参数,最后一个参数必须是 NULL,表示参数列表的结束。

例:

c 复制代码
execl("/bin/sh", "sh", NULL);
  • execlp()

功能:

原型:int execlp(const char *file, const char *arg, ... /* (char *) NULL */);

file:程序的名称,它会根据PATH环境变量查找程序。

arg:程序的第一个参数,通常是程序的名称。

后续的参数依次为程序需要传递的各个参数,最后一个参数必须是 NULL,表示参数列表的结束。

例:

c 复制代码
execlp("sh", "sh", NULL);
  • brk

功能:改变数据段的结束位置,扩展或收缩堆。

原型:int brk(void *end_data);

end_data_segment:指定新的堆的结束位置。如果新位置比当前的堆结束位置要大,brk会扩展堆,否则会缩小堆。

例:

c 复制代码
brk((void *)0x400000);  // 设置数据段结束位置
  • sbrk

功能:sbrk 用于增加或减少数据段的大小,它接受一个相对于当前堆末尾的偏移量。

原型:void *sbrk(intptr_t increment);

increment:堆大小变化的字节数。如果increment为正,堆会增大;如果为负,堆会减小。

返回值:返回修改前的堆的结束地址,如果出错则返回 (void *)-1

例:

c 复制代码
void *current_brk = sbrk(0); //获取当前堆的结束地址
void *new_brk = sbrk(1024);  //数据段的大小增加1024
  • mmap

功能:映射文件或设备到内存,或者分配内存。

原型:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

addr:映射区域的起始地址,如果指定为NULL,内核会选择合适的地址。

length:映射区域的大小,为了页对齐一般为页的整数倍。如果没有对齐mmap会自动处理,通常会向上或向下调整为页大小的整数倍。

prot:映射区域的保护标志,决定了该区域的可读(PROT_READ=1)、可写(PROT_WRITE=2)、可执行(PROT_EXEC=4)及禁止访问(PROT_NONE=0)等权限。

flags:映射的选项,控制映射的行为。MAP_SHARED=1,对映射区域的修改会同步到文件(即文件内容会被修改)。MAP_PRIVATE=2,对映射区域的修改不会影响文件,即是一个私有副本。MAP_ANONYMOUS=0x20,匿名映射,不与任何文件关联,通常用于创建一块不与任何文件内容关联的内存区域。MAP_FIXED=0x10,强制映射到指定的addr地址。MAP_POPULATE=0x8000,提前将映射的页面加载到物理内存中。MAP_LOCKED=0x200,保证映射的内存区域保持在物理内存中,不会被交换到磁盘。

fd:要映射的文件描述符,如果是匿名映射,则设为 -1

offset:文件映射的起始偏移量,必须是页大小的整数倍。

返回值:映射区域的起始地址。如果失败,则返回MAP_FAILED(通常是 (void *)-1)。

例:

c 复制代码
void *addr = mmap(NULL, 0x100 , PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
  • munmap

功能:解除映射的内存区域。

原型:int munmap(void *addr, size_t length);

addr:要解除映射的起始地址。

length:解除映射的区域大小。

例:

c 复制代码
munmap(addr, 0x1000);  // 解除映射
  • mprotect

功能:修改已映射内存区域的保护属性。

原型:int mprotect(void *addr, size_t len, int prot);

void:开始地址(为了页对齐,该地址必须是一个内存页的起始地址,即页大小整数倍,1页=4k=0x1000)

len:指定长度(长度也应该是页的整倍数,即0x1000的整数倍)

prot:指定属性PROT_READ=4(读)、PROT_WRITE=2(写)、PROT_EXEC=1(执行)

例:

c 复制代码
mprotect(addr, 4096, PROT_READ | PROT_WRITE);  // 使内存可读可写

Linux 多线程编程

C语言的多线程编程是通过操作系统提供的线程库(如 POSIX 线程库,简称 pthread)实现的。线程(thread)是比进程更轻量的执行单元,它可以在同一个进程中并行执行代码。C语言本身不直接支持多线程编程,但通过 POSIX 标准或其他库,能够实现多线程编程。

线程与进程的区别

  • 进程:是操作系统资源分配的基本单位,每个进程都有自己独立的内存空间。
  • 线程:是进程的执行单元,多个线程共享进程的内存和资源(如文件描述符)。因此,线程之间的通信比进程间通信(IPC)更高效,但也带来同步问题。

POSIX线程库

pthreads是 POSIX 标准中定义的一组线程库,提供了线程的创建、同步和终止等功能。它在大多数类 Unix 系统上都可用,并且支持多核处理器上并行执行任务。使用pthreads库需要包含pthread.h头文件。

使用了pthreads库的程序在编译时,需要链接pthread库,因此使用-pthread选项:

bash 复制代码
gcc -pthread -o demo demo.c

常用函数和概念

  • 线程创建: pthread_create()
  • 线程等待: pthread_join()
  • 线程取消: pthread_cancel()
  • 线程同步: pthread_mutex_tpthread_cond_t
  • 线程退出: pthread_exit()
  • 线程ID: pthread_self()

线程创建与管理

  • 线程创建

使用 pthread_create() 函数创建一个新线程。该函数的原型如下:

c 复制代码
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

thread: 新线程的 ID

attr: 线程的属性,通常为NULL,使用默认属性

start_routine: 线程执行的函数,必须返回void*,接受一个void*类型的参数

arg: 传递给线程函数的参数

  • 线程等待

pthread_join()用于等待线程的终止,主线程或其他线程在调用此函数时会阻塞,直到指定的线程执行完毕。

c 复制代码
int pthread_join(pthread_t thread, void **retval);

thread: 要等待的线程 ID

retval: 线程返回的值(如果需要)

  • 线程退出

线程执行完毕后,可以调用pthread_exit()显式退出线程,通常线程自动退出时会返回一个值。

c 复制代码
void pthread_exit(void *retval);
  • 线程ID

每个线程都有一个唯一的 ID,可以通过pthread_self()获取当前线程的 ID。

c 复制代码
pthread_t pthread_self(void);
  • 实例

我们使用上面的线程函数写了一段实例代码。代码作用是将全局变量num的值依次加 1 直到加到 10。但是我们可以看到结果并不是我们想的那样。

c 复制代码
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

int num;

void* add(void* ptr) {
	num+=1;
	sleep(0.1);
    printf("Thread %d num = %d\n",(unsigned int)pthread_self(),num);
}

int main() {
    pthread_t thread[10];
    for(int i=0;i<10;i++){
	    pthread_create(&thread[i], NULL, add, NULL);
    }

	for(int i=0;i<10;i++){
	    pthread_join(thread[i], NULL);
    }

	printf("Final num = %d\n", num);
    return 0;
}
  • 运行结果

可以看到,虽然结果是正确的。但是每个线程访问num后的结果却是不同的。这时因为在多线程编程中的一个常见问题共享资源的访问冲突。由于多线程同时访问num变量导致了这样的结果。为了解决这个问题我们需要通过加锁。

复制代码
Thread -1456384448 num = 3
Thread -1464777152 num = 4
Thread -1489955264 num = 7
Thread -1498347968 num = 8
Thread -1506740672 num = 8
Thread -1523526080 num = 10
Thread -1473169856 num = 5
Thread -1531918784 num = 10
Thread -1481562560 num = 7
Thread -1515133376 num = 10
Final num = 10

线程同步

多线程编程中的一个常见问题是共享资源的访问冲突。为了避免这种情况,C 语言提供了同步原语来保护共享资源,常用的同步方式包括互斥锁(mutex)和条件变量。

  • 互斥锁

互斥锁用于保护共享资源,确保同一时间只有一个线程能够访问该资源。

互斥锁有pthread_mutex_t类型,可以通过pthread_mutex_init()初始化,pthread_mutex_lock()获取锁,pthread_mutex_unlock() 解锁。

c 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&mutex);
// 临界区,访问共享资源
pthread_mutex_unlock(&mutex);
  • 条件变量

条件变量用于在某个条件不成立时阻塞线程,并在条件满足时唤醒线程。条件变量与互斥锁一起使用,确保条件判断和资源访问的原子性。

c 复制代码
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&mutex);
while (条件不满足) {
    pthread_cond_wait(&cond, &mutex);
}
// 条件满足时,执行后续操作
pthread_mutex_unlock(&mutex);

pthread_cond_wait()会释放互斥锁并使线程阻塞,直到条件满足时通过pthread_cond_signal()pthread_cond_broadcast()唤醒等待线程。

  • 实例
c 复制代码
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
  
int num;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  // 初始化互斥锁
  
void* add(void* ptr) {
    // 加锁,确保每次只有一个线程能修改 num
    pthread_mutex_lock(&mutex);
    num += 1;
    sleep(0.1);
    printf("Thread %d num = %d\n", (unsigned int)pthread_self(), num);
    pthread_mutex_unlock(&mutex);  // 解锁
}

int main() {
    pthread_t thread[10];
    for(int i = 0; i < 10; i++) {
        pthread_create(&thread[i], NULL, add, NULL);
    }

    for(int i = 0; i < 10; i++) {
        pthread_join(thread[i], NULL);
    }
  
    printf("Final num = %d\n", num);
    return 0;
}

网络编程

基础知识

Linux 网络编程涉及在 Linux 操作系统上进行网络通信的技术。通常,网络编程包括使用套接字(socket)进行网络连接和数据传输。套接字是应用层与网络层之间的接口,是实现网络通信的核心工具。

在 C 语言中,套接字编程通常是基于 POSIX 标准,通过一系列函数来创建、管理、连接和传输数据。

在使用套接字编程时通常我们需要包含以下头文件。

c 复制代码
#include <sys/socket.h> //套接字编程的核心头文件
#include <netinet/in.h> //主要用于处理ipv4地址及相关操作
#include <arpa/inet.h>  //主要处理点分十进制与二进制地址之间的转换

套接字基础

套接字是通信的端点,允许应用程序在计算机之间传输数据。它提供了操作系统和网络之间的接口,支持多种通信协议(如 TCP、UDP)。

常见套接字类型:

  • TCP(流式套接字):可靠的、面向连接的通信,基于流的协议。
  • UDP(数据报套接字):无连接的协议,适用于对实时性要求高的场景,如视频流、在线游戏等。

常见套接字函数

  • socket():创建一个套接字并返回套接字描述符。
c 复制代码
int socket(int domain, int type, int protocol);

domain:指定协议族。常用的有AF_INETIPv4协议,AF_INET6IPv6协议, AF_UNIX本地通信协议(Unix域套接字)。

type:指定套接字类型。SOCK_STREAMTCP套接字(面向连接),SOCK_DGRAMUDP套接字(无连接)SOCK_RAW:原始套接字。

protocol,指定协议,通常设置为0,由domaintype决定。

返回值:成功时返回一个套接字描述符,失败时返回-1

  • bind():将套接字绑定到一个特定的地址(IP和端口)。
c 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfd:套接字描述符。

addr:指向struct sockaddr类型的地址结构。

addrlen:地址结构的长度。

返回值:成功返回0,失败返回-1

  • listen():使套接字处于监听状态,等待客户端的连接请求。
c 复制代码
int listen(int sockfd, int backlog);

sockfd:套接字描述符。

backlog:指定监听队列的最大长度。

返回值:成功返回0,失败返回-1

  • accept():接受来自客户端的连接请求,并返回一个新的套接字描述符用于与客户端通信。
c 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

sockfd:监听套接字描述符。

addr:客户端的地址信息。

addrlen:地址信息的长度。

返回值:成功时返回新的套接字描述符,失败时返回-1

  • connect():客户端连接到服务器。
c 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfd:客户端套接字描述符。

addr:服务器的地址信息。

addrlen:地址信息的长度。

返回值:成功返回0,失败返回-1

  • recv():从套接字接收数据。
c 复制代码
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

sockfd:套接字描述符。

buf:接收数据的缓冲区。

len:缓冲区的大小。

flags:接收的标志,通常为0

返回值:成功时返回接收到的字节数,失败时返回-1

  • recvfrom():与recv()类似,但用于无连接协议(如UDP),可以获取发送者的地址。
c 复制代码
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);

src_addr:发送者的地址信息。

addrlen:地址结构的长度。

  • send():向套接字发送数据。
c 复制代码
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

sockfd:套接字描述符。

buf:要发送的数据。

len:数据的大小。

flags:发送的标志,通常为0

返回值:成功时返回发送的字节数,失败时返回-1

  • sendto():与send()类似,但用于无连接协议(如UDP),可以指定目的地址。
c 复制代码
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

dest_addr:目的地的地址信息。

addrlen:地址结构的长度。

  • close():关闭套接字。
c 复制代码
int close(int sockfd);

返回值:成功返回0,失败返回-1

  • getsockopt():获取套接字的选项设置。
c 复制代码
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);

sockfd:套接字描述符。

level:指定选项所在的协议层(例如SOL_SOCKET)。

optname:选项名称(例如SO_REUSEADDR)。

optval:指向存放选项值的缓冲区。

optlen:选项值的长度。

返回值:成功返回0,失败返回-1

  • setsockopt():设置套接字的选项。
c 复制代码
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

sockfd:套接字描述符。

level:指定选项所在的协议层(例如SOL_SOCKET)。

optname:选项名称(例如SO_REUSEADDR)。

optval:指向选项值的缓冲区。

optlen:选项值的长度。

返回值:成功返回0,失败返回-1

  • getsockname():获取本地套接字地址。
c 复制代码
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

sockfd:套接字描述符。

addr:存储本地地址信息的结构。

addrlen:地址结构的长度。

  • getpeername():获取远程套接字的地址。
c 复制代码
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

sockfd:套接字描述符。

addr:存储远程地址信息的结构。

addrlen:地址结构的长度。

  • select():用于监视多个套接字,判断它们是否可读、可写或发生错误。
c 复制代码
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

nfds:文件描述符集合的大小。

readfds:可读的文件描述符集合。

writefds:可写的文件描述符集合。

exceptfds:出错的文件描述符集合。

timeout:等待时间。

数据结构

下面我们来了解一下在网络编程中常见的数据结构。

  • struct sockaddr_in

功能:常用于描述 IPV4 网络地址的结构体,定义在netinet/in.h头文件中。

定义:

c 复制代码
struct sockaddr_in {
    short            sin_family;    // 地址族,通常为 AF_INET(IPv4)
    unsigned short   sin_port;      // 端口号,使用网络字节序
    struct in_addr   sin_addr;      // IP 地址
    char             sin_zero[8];   // 填充,未使用的区域
};
  • socklen_t

功能:这个类型定义了套接字地址的长度,在 Linux 系统它就是一个int型。定义在sys/socket.h头文件中。

实例

  • TCP 服务器
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    // 设置程序监听端口为8080
    long long port = 8080;
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buffer[1024];
 
    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket failed");
        exit(1);
    }

    // 配置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(port);

    // 绑定套接字与地址
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(server_fd);
        exit(1);
    }

    // 监听客户端连接
    if (listen(server_fd, 3) == -1) {
        perror("listen failed");
        close(server_fd);
        exit(1);
    }
  
    printf("Server listening port %d\n", (int)port);

    // 接受客户端连接
    if ((client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len)) == -1) {
        perror("accept failed");
        close(server_fd);
        exit(1);
    }

    printf("Connection from port %d\n",(int)port);
  
    // 接收客户端消息并回复
    while (1) {
        int len = recv(client_fd, buffer, sizeof(buffer), 0);
        if (len <= 0) {
            printf("Not received\n");
            break;
        }

		buffer[len] = '\0'; // 确保字符串结束
        printf("Port %d receive data: %s\n", (int)port,buffer);
        printf("Send to client: ");
        fgets(buffer, sizeof(buffer), stdin);
        buffer[strcspn(buffer, "\n")];
		// 回复消息
        send(client_fd, buffer, sizeof(buffer), 0);
    }

    // 关闭连接
    close(client_fd);
    close(server_fd);
    return 0;
}
  • TCP 客户端
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    long long port=8888;
    int sock;
    struct sockaddr_in server_addr;
    char buffer[1024];

    // 创建套接字
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket failed");
        exit(1);
    }

    // 配置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器地址

    // 连接到服务器
    if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect failed");
        close(sock);
        exit(1);
    }
  
    printf("Connected to server\n");
  
    // 向服务器发送消息
    while (1) {
        printf("Send client: ");
        fgets(buffer, sizeof(buffer), stdin);
        buffer[strcspn(buffer, "\n")] = '\0'; // 去除末尾换行符
        send(sock, buffer, strlen(buffer), 0);

		// 接收服务器的响应
        int len = recv(sock, buffer, sizeof(buffer), 0);
        if (len <= 0) {
            printf("Not received\n");
            break;
        }
        buffer[len] = '\0';
        printf("Received data: %s\n", buffer);
    }

    // 关闭连接
    close(sock);
    return 0;

}
  • 多线程 TCP 服务器
c 复制代码
#include <stdio.h>

#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>

void* conn(void* arg) {
    long long port = *(long long*)arg;
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buffer[300];
  
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket failed");
        exit(1);
    }
   
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(port);
  
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(server_fd);
        exit(1);
    }

     if (listen(server_fd, 3) == -1) {
        perror("listen failed");
        close(server_fd);
        exit(1);
    }

    printf("Server listening port %d\n", (int)port);

    if ((client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len)) == -1) {
        perror("accept failed");
        close(server_fd);
        exit(1);
    }

    printf("Connection from port %d\n",(int)port);
 
    while (1) {
        int len = recv(client_fd, buffer, sizeof(buffer), 0);
        if (len <= 0) {
            printf("Not received\n");
            break;
        }

        buffer[len] = '\0';
        printf("Port %d receive data: %s\n", (int)port,buffer);
        printf("Send to client: ");
        fgets(buffer,sizeof(buffer),stdin);
        buffer[strcspn(buffer, "\n")];
        send(client_fd, buffer, sizeof(buffer), 0);
    }
    close(client_fd);
    close(server_fd);

    return NULL;
}

int main() {

    long long port []= {80,8080,8888,9999};
    pthread_t thread[sizeof(port)/sizeof(port[0])];

    for(int i=0; i < sizeof(port)/sizeof(port[0]); i++){
        pthread_create(&thread[i], NULL, conn, (void*)&port[i]);
    }
    for(int i=0; i < sizeof(port)/sizeof(port[0]); i++){
        pthread_join(thread[i], NULL);
    }

    return 0;
}
相关推荐
海清河晏1114 小时前
Linux进阶篇:HTTP协议
linux·运维·http
Morwit4 小时前
Qt qml创建c++类的单例对象
开发语言·c++·qt
June`4 小时前
IO模型全解析:从阻塞到异步(高并发的reactor模型)
linux·服务器·网络·c++
古城小栈4 小时前
Rust 已经自举,却仍需GNU与MSVC工具链的缘由
开发语言·rust
ASS-ASH4 小时前
快速处理虚拟机磁盘扩容问题
linux·数据库·vmware·虚拟机·磁盘扩容
AI_56784 小时前
零基础学Linux:21天从“命令小白”到独立部署服务器
linux·服务器·人工智能·github
jarreyer4 小时前
数据项目分析标准化流程
开发语言·python·机器学习
不染尘.4 小时前
Linux基本概述
linux·windows·centos·ssh
你怎么知道我是队长4 小时前
C语言---printf函数使用详细说明
c语言·开发语言