前言
本文的目的是对学 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
功能:snprintf是sprintf的安全版本,防止缓冲区溢出。
原型: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系统上。
管道的基本概念
管道有两种类型:
-
匿名管道(Anonymous Pipe) :用于具有亲缘关系(父子进程或者兄弟进程)之间的进程通信。匿名管道通常在创建进程时由
fork系统调用自动创建。 -
命名管道(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:用来保存子进程的退出状态,可以通过WIFEXITED、WEXITSTATUS等宏分析子进程的退出情况。
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_t、pthread_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,由domain和type决定。
返回值:成功时返回一个套接字描述符,失败时返回-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;
}