linux应用层

45 阅读1小时+

linux应用程序开发

linux操作系统上应用程序的开发

文件io

  • i: input 输入
  • o:output 输出
  • linux中万物皆文件
  • FILE* 表示文件

1.文件描述符号

  • fd 是File Describe的简写
  • fd和文件之间如果经历了打开这一动作,就会建立双向映射,两端有任何一端变化,另一端也会跟随变化。
  • c语言中操作文件有缓冲,linux操作系统中无缓冲

2.文件内容操作

  • 操作系统自带的函数,返回值默认规则
  • 如果执行成功返回0
  • 如果执行成功并且有返回值,返回正整数
  • 如果执行失败,返回负数,不同的负数代表不同的失败原因,
  • 执行失败的错误描述会写到操作系统维护的一块内存段中,并且通过%m以printf("%m")的方式可以打印
2.1 linux文件的打开与关闭
代码功能参数返回值其他
int fd = open("1.txt",O_CREAT|O_WRONLY,0666)在linux系统中打开文件,并复制给文件描述符参数:要打开的文件路径打开方式权限(只有当打开方式是创建O_CREATEc才需要,格式0666)成功为文件描述符(非负整数),失败-1O_RDONLY: 只读方式打开。O_WRONLY: 只写方式打开。 O_RDWR: 读写方式打开。O_CREAT: 如果文件不存在则创建
close(fd)关闭文件需要关闭的文件描述符成功返回0,失败返回-1
2.2 文件的读写
代码功能参数返回值其他
int r = read(fd,&s,sizeof s);将文件描述符变量fd的内容读取出来放到s结构体中参数:需要读取内容的文件描述符储存读取到数据的指针变量读入的字节数成功返回实际读取的字节数,失败返回-1,0表示读取到了内容最末尾fd为0时,和scanf一样的效果,会读取键盘输入
int r = write(fd,s,sizeof(Student)*NUM);将s结构体链表的数据写入到文件描述符变量fd参数:需要写入内容的文件描述符指向内存中数据的指针写入的字节数成功返回实际写入的字节数,失败返回-1fd为1或2时,和printf打印一样的效果
c^=0x66;^按位异或操作将文件用read读出来后,执行这句,在用write写进去,能达到简易加密效果
2.3 文件内容指针移动(光标移动)
代码功能参数返回值其他
int r = lseek(fd,2,SEEK_CUR);操作文件描述符(open打开的),将内容光标,从末尾位置开始计算,向前移动2个位置参数:需要移动内容光标的文件描述符要移动的字节数基准位置成功返回新的文件偏移量(从文件开头计算的字节数),失败返回-1SEEK_SET从头开始计算偏移量SEEK_CUR从当前偏移量位置开始计算偏移量SEEK_END从文件末尾开始计算偏移量偏移量正数后移,负数前移
int r = fseek(fd,0,SEEK_SET);操作文件流(fopen打开的),将内容光标移动到文件头参数:需要移动内容光标的文件流要移动的字节数基准位置成功返回新的文件偏移量(从文件开头计算的字节数),失败返回-1SEEK_SET从头开始计算偏移量SEEK_CUR从当前偏移量位置开始计算偏移量SEEK_END从文件末尾开始计算偏移量偏移量正数后移,负数前移
size_t filesize = ftell(fp);获取文件流中的当前位置指示器(也称为文件指针)的偏移量参数:需要移动内容光标的文件流长整型值,表示从文件开头到当前文件指针之间的字节数,失败返回-1L
2.3 三个特殊的文件描述符

在linux中,有三个不用再去open,默认就已经打开了的文件描述符

名称简写功能其他
stdin0程序读取输入的地方,默认是键盘read(0,buff,255)fd为0这种方式使用可以当scanf用,会把键盘输入的内容写到buff变量
stdout1程序输出正常信息的地方,默认是终端屏幕write(1, buff, 255)fd为1这种方式使用可以当printf用,会在终端打印buff
stderr2程序输出错误信息的地方,默认是终端屏幕

2.文件操作

代码功能参数返回值其他
system("ls");执行shell命令shell命令命令的执行效率相对应用程序低一些。因为命令需要启动shell来完成工作。
unlink("test.txt")删除test.txt文件要删除的文件路径(需包含文件名)0成功,-1失败只能删除文件,不能删除目录
stat("test.txt")获取文件信息文件路径通过文件名直接访问文件信息
int fstat(int fd, struct stat *buf);获取文件信息文件描述符,指向 struct stat 类型的指针,用于存储获取到的文件信息成功0,失败-1和stat类似,只是一个用路径获取,一个用文件描述符获取

目录

  • 目录在linux中也是一个文件
  • 通过vi编辑器,可以打开目录
代码功能参数返回值其他
DIR* pDir = opendir(".");打开当前目录,创建一个目录流需要打开的目录返回 DIR* 类型的目录流指针,返回 NULL
pd = readdir(pDir);读取目录流的内容目录流struct dirent结构体的目录流的内容结构体有三个字段分别是d_ino编号、d_name文件名、d_type文件类型

文件映射虚拟内存

什么叫做文件映射虚拟内存

文件映射虚拟内存是一种将文件直接映射到进程虚拟地址空间的技术,使得文件操作如同访问内存一样高效。其核心思想是通过操作系统的页表机制,将文件内容与内存地址动态关联,避免传统read/write的系统调用开销。

  • 通过一系列的步骤,把文件变成一块内存段,用操作内存的方式来操作文件。
  • 内存操作:赋值、strcpy、memcpy、指针的数学运算等等
  • 文件操作:读read、写write、移动内容光标lseek等等
  • 文件映射虚拟内存使用方便,执行效率高,内存操作的效率远远高于io(文件输入输出)操作效率

文件虚拟内存使用

扩展文件 ftruncate
/**
 * 扩展文件系统的ftruncate函数
 * 
 * 该程序演示了如何使用ftruncate函数来调整文件的大小。程序首先创建并打开一个文件,
 * 然后使用ftruncate函数将文件大小设置为1024字节。如果任何操作失败,程序将输出错误信息并退出。
 */

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>

int main(){
    // 打开或创建文件 "test.day2",并设置文件权限为 0666(可读可写)
    int fd = open("test.day2", O_RDWR | O_CREAT, 0666);
    if(-1 == fd) {
        // 如果文件打开失败,输出错误信息并退出程序
        printf("创建并打开文件失败:%m\n");
        exit(-1);
    }
    printf("创建并打开文件成功!\n");

    // 使用 ftruncate 函数将文件大小设置为 1024 字节
    int r = ftruncate(fd, 1024);
    if(-1 == r) {
        // 如果 ftruncate 调用失败,输出错误信息,关闭文件并退出程序
        printf("ftruncate调用失败:%m\n");
        close(fd);
        exit(-1);
    }
    printf("r:%d\n", r);

    // 关闭文件
    close(fd);
    return 0;
}
截断文件 truncate
/**
 * 截断文件的truncate函数
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

/**
 * 安全地截断文件到指定长度
 * 
 * @param path 要截断的文件路径
 * @param length 截断后的文件长度(字节数)
 * @return 成功返回0,失败返回-1并打印错误信息
 */
int safe_truncate(const char *path, int length) {
    // 调用truncate并检查返回值
    if (truncate(path, length) == -1) {
        // 如果截断失败,打印错误信息并返回-1
        printf("截断文件 '%s' 失败: %s\n", path, strerror(errno));
        return -1;
    }

    // 截断成功,打印成功信息并返回0
    printf("文件 '%s' 截断成功,新长度为 %d 字节\n", path, length);
    return 0;
}

/**
 * 主函数,用于测试safe_truncate函数
 * 
 * @return 成功返回EXIT_SUCCESS,失败返回EXIT_FAILURE
 */
int main() {
    const char *path = "/path/to/file";
    int length = 1024;

    // 调用safe_truncate函数并检查返回值
    if (safe_truncate(path, length) == -1) {
        // 如果截断失败,打印失败信息并返回EXIT_FAILURE
        printf("截断文件失败\n");
        return EXIT_FAILURE;
    }

    // 截断成功,返回EXIT_SUCCESS
    return EXIT_SUCCESS;
}
mmap 映射内存
/**
 * 创建一个文件,用映射内存的方式往文件里写入整数
*/
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/mman.h>

/**
 * @brief 主函数,演示了如何使用文件映射(mmap)将数据写入文件。
 * 
 * 该函数首先创建并打开一个文件,然后使用ftruncate设置文件大小,
 * 接着使用mmap将文件映射到内存中,并在内存中写入数据,最后解除映射并关闭文件。
 * 
 * @return int 返回0表示程序正常结束,返回-1表示出现错误。
 */
int main(){
    // 创建并打开文件 "test.day2",如果文件不存在则创建,权限设置为0666
    int fd = open("test.day2",O_RDWR | O_CREAT  ,0666);
    if(-1 == fd) printf("创建并打开文件失败:%m\n"),exit(-1);
    printf("创建并打开文件成功!\n");

    // 使用ftruncate将文件大小设置为4字节
    int r = ftruncate(fd,4);
    printf("r:%d\n",r);

    // 使用mmap将文件映射到内存中,映射大小为4字节,权限为可写 
    // 参数依次为: NULL表示让系统选择映射地址,4表示映射大小,权限为可写,MAP_SHARED表示映射到共享内存中,fd表示文件描述符,0表示映射偏移量
    int* p = (int*)mmap(NULL,4,
        PROT_WRITE,
        MAP_SHARED,
        fd,0);

    // 检查mmap是否成功,如果失败则输出错误信息并退出程序
    if((int*)-1 == p){
        printf("mmap error:%m\n"),close(fd),exit(-1);
    }
    printf("mmap %m\n");

    // 在映射的内存中写入数据
    *p = 6666666;

    // 解除内存映射并关闭文件
    munmap(p,4);
    close(fd);
    printf("over!\n");
    return 0;
}
/**
 * 打开mmapA创建的文件,映射为虚拟内存,然后读出数据并打印到终端。
*/
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/mman.h>

/**
 * @brief 主函数,演示如何使用mmap将文件映射到内存并读取文件内容。
 * 
 * 该函数首先打开一个文件,然后使用mmap将文件内容映射到内存中,最后读取并打印文件内容。
 * 完成后,解除内存映射并关闭文件。
 * 
 * @return int 返回0表示程序正常结束。
 */
int main(){
    // 打开文件"test.day2",以只读模式
    int fd = open("test.day2",O_RDONLY);
    if(-1 == fd) printf("创建并打开文件失败:%m\n"),exit(-1);
    printf("创建并打开文件成功!\n");

    // 使用mmap将文件的前4个字节映射到内存中
    // 参数:映射的起始地址,映射的大小,映射的权限,映射的方式,映射的文件描述符,映射的文件偏移量
    int* p = (int*)mmap(NULL,4,
        PROT_READ,
        MAP_SHARED,
        fd,0);

    // 检查mmap是否成功
    if((int*)-1 == p){
        printf("mmap error:%m\n"),close(fd),exit(-1);
    }
    printf("mmap %m\n");

    // 打印映射到内存中的文件内容
    printf("文件内容:%d\n",*p);
   
    // 解除内存映射,参数: 映射的起始地址,映射的大小
    int r = munmap(p,4);
    printf("解除映射:%d\n",r);

    // 关闭文件
    close(fd);
    printf("over!\n");
    return 0;
}

进程

  • 进程是程序的执行实例,拥有独立的地址空间、资源(如文件描述符、信号处理)及执行状态
  • 进程控制块 (PCB):Linux 中为 task_struct 结构体,存储进程 ID、状态、优先级、内存映射、打开文件等信息。
  • 进程是一个运行中的程序。源程序文件。目标文件。可执行程序文件。
  • 进程是操作系统资源调度的基本单位。

进程的组成部分

  • 源程序文件(代码),进程id,权限,时钟,用户等

进程的状态

  • 运行时状态,挂起,睡眠,死亡,僵尸
  1. TASK_RUNNING:正在运行或就绪状态,等待 CPU 调度。
  2. TASK_INTERRUPTIBLE:可中断睡眠(等待资源,可被信号唤醒)。
  3. TASK_UNINTERRUPTIBLE:不可中断睡眠(等待硬件操作,如磁盘 I/O)。
  4. TASK_STOPPED:暂停状态(如收到 SIGSTOP 信号)。
  5. EXIT_ZOMBIE:僵尸状态(进程已终止,但父进程未回收资源)。
  6. EXIT_DEAD:最终状态,等待系统清理。
stateDiagram
    [*] --> TASK_RUNNING
    TASK_RUNNING --> TASK_INTERRUPTIBLE: 等待资源
    TASK_RUNNING --> TASK_UNINTERRUPTIBLE: 等待硬件操作
    TASK_RUNNING --> TASK_STOPPED: 收到SIGSTOP
    TASK_INTERRUPTIBLE --> TASK_RUNNING: 资源就绪或信号
    TASK_UNINTERRUPTIBLE --> TASK_RUNNING: 硬件操作完成
    TASK_STOPPED --> TASK_RUNNING: 收到SIGCONT
    TASK_RUNNING --> EXIT_ZOMBIE: 进程终止
    EXIT_ZOMBIE --> [*]: 父进程调用wait()

进程监控与管理工具

命令功能
ps查看进程快照(如 ps aux
top实时监控进程资源使用
htop增强版交互式进程监控器
pstree以树形结构显示进程关系
strace跟踪进程的系统调用
/proc虚拟文件系统,提供进程详细信息(如 /proc/[pid]/status

进程调度

完全公平调度器 (CFS)
  • 使用红黑树跟踪进程的虚拟运行时间(vruntime),确保公平分配 CPU 时间。
  • 优先级:通过 nice 值调整(范围 -20 到 19,值越小优先级越高)。
  • 实时进程:使用 SCHED_FIFO 或 SCHED_RR 策略,抢占普通进程。
  • ps -eo pid,ni,comm 查看进程优先级
优先级调度机制
  • 给进程设置不同的优先级,优先执行优先级高的,按次序往后执行优先级低的。

进程创建与管理

执行程序
  • 执行 a.out
  • 执行命令 system("ls")等
fork 创建子进程
原理

子进程就是将整个c文件,包括头文件,全局变量,主函数,子函数,都复制了一遍

  1. fork函数创建子进程后,父子进程同时往下运行,并发执行!
  2. 子进程拷贝父进程所有代码,并且拷贝父进程的进程上下文。 a. 进程上下文:进程执行到哪了。 b. 父进程的fork函数返回子进程id,子进程的fork函数返回0
  3. 子进程拷贝父进程的资源
  4. copy_on_wirte a. 子进程不是一开始就拷贝父进程的资源。 b. 而是在需要写访问资源的时候再去拷贝。 c. 不需要写,只需要读,那就父子进程共用。
  5. 子进程结束前给父进程发送SIGCHLD信号。
示例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

/**
 * @brief 主函数,演示fork()创建子进程,并分别在父进程和子进程中执行循环打印。
 * 
 * 该程序通过调用fork()创建一个子进程。父进程和子进程分别进入不同的循环,
 * 每秒打印一次进程ID、父进程ID和一个递增的计数器。
 * 
 * @return int 返回值为0,表示程序正常结束。
 */
int main(){
    int n = 0;
    pid_t pid = fork();  // 创建子进程,返回子进程的PID给父进程,返回0给子进程

    if(pid){
        // 父进程执行的代码块
        while(1){
            // 打印父进程的PID、父进程的父进程ID以及一个递增的计数器
            printf("程序%d %d执行%d!\n",getpid(),getppid(),n++);
            sleep(1);  // 暂停1秒
        }
    }else{
        // 子进程执行的代码块
        while(1){
            // 打印子进程的PID、父进程ID以及一个递增的计数器
            printf("程序%d %d执行%d!\n",getpid(),getppid(),n+=2);
            sleep(1);  // 暂停1秒
        }
    }
    return 0;
}

父子进程的n互相不干扰

image.png

exec函数族
功能

exec()函数族用于将当前进程的代码段、数据段和堆栈替换为新程序的映像,但保留原进程的 PID、文件描述符和信号处理。常用于在fork()创建子进程后执行新程序。

函数族成员
函数名参数格式环境变量路径搜索
execl参数列表(可变参数)继承父进程环境需绝对/相对路径
execv参数数组(char *argv[]继承父进程环境需绝对/相对路径
execlp参数列表(可变参数)继承父进程环境使用 PATH 环境变量
execvp参数数组(char *argv[]继承父进程环境使用 PATH 环境变量
execle参数列表(可变参数) + 自定义环境变量自定义环境变量(需显式传递)需绝对/相对路径
execvpe参数数组 + 自定义环境变量自定义环境变量使用 PATH 环境变量
函数族参数说明

通用参数

  • path:新程序的路径(如 "/bin/ls")。
  • arg0:程序名称(通常与 path 最后一个 / 后的名称一致)。
  • ...:参数列表,以 NULL 结尾。
  • argv[]:参数数组,最后一个元素为 NULL。 环境变量:execle 和 execvpe 需要显式传递环境变量数组(char *envp[]),格式为 "KEY=VALUE" 字符串数组。 成功:无返回值(原进程映像被替换,后续代码不再执行)。 失败:返回 -1,并设置 errno(如文件不存在、权限不足等)。

示例

    execl("/bin/ls", "ls", "-l", NULL); // 执行ls -l
3. wait() / waitpid()
  • 功能:父进程回收子进程资源,避免僵尸进程。

  • 示例

wait 等待任意一个子进程结束就执行
   #include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

/**
 * @brief 主函数,演示父子进程的创建和执行。
 * 
 * 该程序通过fork()创建子进程,父进程和子进程分别执行不同的循环任务。
 * 父进程等待子进程结束,并打印子进程的退出状态。
 * 
 * @return int 程序执行成功返回0。
 */
int main()
{
    // 创建子进程
    if (fork())
    {
        // 父进程代码块
        // 父进程循环5次,每次打印当前迭代次数、进程ID和父进程ID
        for (int i = 0; i < 5; i++)
        {
            printf("父进程:%d %d %d\n", i + 1, getpid(), getppid());
            sleep(1);
        }
        // 等待子进程结束,并获取子进程的退出状态
        int ret = 0;
        wait(&ret);
        // 打印子进程的退出状态
        printf("ret:%d\n", ret);
    }
    else
    {
        // 子进程代码块
        // 子进程循环15次,每次打印当前迭代次数、进程ID和父进程ID
        for (int i = 0; i < 7; i++)
        {
            printf("子进程:%d %d %d\n", i + 1, getpid(), getppid());
            sleep(1);
        }
        // 子进程返回退出状态7
        return 7;
    }
    return 0;
}

父进程打印五次后,子进程继续打印两次,wait才会执行,然后打印子进程退出状态 image.png

waitpid 通过id指定等待子进程结束
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

/**
 * main - 主函数,演示了如何使用 fork() 创建子进程,并在子进程中执行命令,父进程等待子进程结束。
 * 
 * 该程序首先通过 fork() 创建一个子进程。子进程执行 "ls -l" 命令,父进程则等待子进程结束,
 * 并根据子进程的退出状态输出相应的信息。
 * 
 * 返回值:
 *   0 - 程序正常结束
 *   1 - fork() 或 waitpid() 失败
 */
int main() {
    pid_t pid = fork();

    // 检查 fork() 是否成功
    if (pid == -1) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程执行命令:ls -l
        printf("Child process (PID=%d) running...\n", getpid());
        execl("/bin/ls", "ls", "-l", NULL);
        exit(EXIT_FAILURE); // 若 exec 失败则退出
    } else {
        // 父进程等待特定子进程结束
        int status;
                /**
         * 等待指定子进程的状态变化,并获取其退出状态。
         *
         * @param pid 要等待的子进程的进程ID。如果pid为-1,则等待任意子进程。
         * @param status 用于存储子进程退出状态的指针。可以通过宏函数(如WIFEXITED、WEXITSTATUS等)来解析该状态。
         * @param options 控制waitpid行为的选项。0表示默认行为,即阻塞等待子进程退出。
         *
         * @return 成功时返回子进程的进程ID;如果指定了WNOHANG选项且没有子进程退出,则返回0;出错时返回-1。
         */
        pid_t child_pid = waitpid(pid, &status, 0);

        // 检查 waitpid() 是否成功
        if (child_pid == -1) {
            perror("waitpid failed");
            exit(EXIT_FAILURE);
        }

        // 根据子进程的退出状态输出相应信息
        if (WIFEXITED(status)) {
            printf("Child (PID=%d) exited with code: %d\n", child_pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Child (PID=%d) killed by signal: %d\n", child_pid, WTERMSIG(status));
        }
    }

    return 0;
}
wait() 与 waitpid() 的区别及使用场景
特性​**wait()**​**waitpid()**
等待目标等待任意一个子进程结束可指定等待特定子进程进程组
阻塞行为必须阻塞直到有子进程结束支持非阻塞模式​(WNOHANG 选项)
灵活性功能简单,适用于简单场景更灵活,支持更多控制选项
等价关系wait(&status) 等价于 waitpid(-1, &status, 0)可模拟 wait(),但功能更强大

进程间通信

广义进程间通信

只要能在两个进程之间传输数据,就叫进程间通信。

  • 普通文件
  • mmap
/**
* 按回车卡住,输入6按回车继续
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <time.h>
#include <sys/time.h>

#define FILENAME "parent.child"

/**
 * 主函数,实现父子进程之间的通信和控制。
 * 父进程生成随机数并输出,子进程通过共享内存控制父进程的输出状态。
 * 无参数,返回值为0表示程序正常结束。
 */
int main(){
    // 初始化随机数种子,确保每次运行生成的随机数不同
    srand(time(NULL));
    
    // 打开或创建文件,用于共享内存映射
    int fd = open(FILENAME, O_RDWR | O_CREAT, 0666);
    
    // 调整文件大小为int类型的大小
    ftruncate(fd, sizeof(int));
    
    // 将文件映射到内存,返回映射区域的指针 参数:文件描述符,映射区域的大小,映射区域的权限,映射区域的类型,映射区域的文件描述符,映射区域的偏移量
    int* p = mmap(NULL, sizeof(int), PROT_WRITE, MAP_SHARED, fd, 0);
    
    // 初始化共享内存中的值为0
    *p = 0;

    // 创建子进程
    if(fork()){
        // 父进程代码块
        while(1){
            // 当*p为1时,父进程被阻塞,等待子进程将其置为0
            while(*p);
            
            // 生成并输出一个随机数
            printf("%8d\n", rand() % 100000000);
            
            // 父进程休眠100毫秒
            usleep(100000);
        }
    }else{
        // 子进程代码块
        int n;
        while(1){
            // 从标准输入读取一个整数
            scanf("%d", &n);
            
            // 如果输入为6,则切换共享内存中的值
            if(6 == n){
                if(*p){
                    *p = 0;
                }else{
                    *p = 1;
                }
            }
        }
    }

    return 0;
}
  • 管道
  • 信号
  • 共享内存
  • 消息队列
  • 信号量(旗语)
  • 网络

狭义进程间通信 IPC

共享内存 消息队列 信号量

管道

  • 管道就是像水管。是特殊的文件。一边写,另一边读。
  • 阻塞的,读端没有读走数据,写端无法再写入
  • 匿名管道只能应用于父子进程之间。
  • 有名管道能且只能应用于能访问同一文件系统的进程之间。
  • 共享文件夹内无法使用管道
  • linux操作系统上 文件的后缀没有意义。
shell里的管道

上一个命令的结果通过管道传递给下一个命令 find ./ find.c | grep printf

匿名管道 pipe()

没有文件,只有文件描述符号;能且只能应用于父子进程之间

#include <stdio.h>
#include <unistd.h>

/**
 * 主函数,用于演示父子进程通过管道进行通信。
 * 父进程从管道中读取数据并打印,子进程向管道中写入数据。
 * 
 * @return 返回0表示程序正常结束。
 */
int main(){
    int p[2];  // 用于存储管道的文件描述符,p[0]为读端,p[1]为写端

    // 创建管道,r为返回值,成功返回0,失败返回-1。参数为:p[0]为读端,p[1]为写端
    int r = pipe(p);
    printf("r:%d\n",r);

    // 创建子进程
    if(fork()){
        // 父进程代码块
        int n;
        while(1){
            // 从管道读取数据,r为读取的字节数
            r = read(p[0],&n,sizeof (int) );
            if(r>0){
                // 如果读取成功,打印读取到的整数
                printf("%d\n",n);
            }
        }
    }else{
        // 子进程代码块
        int m = 0;
        while(1){
            // 向管道写入数据
            write(p[1],&m,sizeof(int));
            sleep(1);  // 等待1秒
            m++;  // 递增写入的值
        }
    }

    return 0;
}
命名管道 mkfifo()

有文件,和普通文件的区别在于它一边读,另外一边只能写。只能两边同时打开。

创建管道并写入
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>  
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#define PIPE_NAME "my.pipe"

/**
 * 主函数:创建一个命名管道,读取管道中的数据,并在接收到特定消息时退出。
 * 
 * 该程序首先创建一个命名管道,然后以只读方式打开该管道。程序进入一个循环,
 * 不断从管道中读取数据并打印到控制台。当读取到“退出”消息时,程序关闭管道并删除管道文件。
 * 
 * @return int 程序退出状态码,0表示正常退出,-1表示异常退出。
 */
int main(){
    // 创建命名管道文件,权限设置为0666(所有用户可读写)参数:PIPE_NAME为管道文件名,0666为权限
    int r = mkfifo(PIPE_NAME,0666);
    if(-1 == r) printf("创建管道失败:%m\n"),exit(-1);
    printf("创建管道成功!\n");

    // 以只读方式打开管道文件
    int fd = open(PIPE_NAME,O_RDONLY);
    if(-1 == fd) printf("打开管道失败:%m\n"),unlink(PIPE_NAME),exit(-1);
    printf("打开管道文件成功!\n");

    // 进入循环,不断从管道中读取数据
    char buff[256];
    while(1){
        memset(buff,0,256); // 清空缓冲区
        r = read(fd,buff,255); // 从管道中读取最多255字节的数据
        if(r > 0){
            buff[r] = 0; // 在读取的数据末尾添加字符串结束符
            printf(">> %s\n",buff); // 打印读取的数据
            if(0 == strcmp("退出",buff)) break; // 如果读取到“退出”消息,则退出循环
        }
    }

    // 关闭管道文件
    close(fd);
    // 删除管道文件
    unlink(PIPE_NAME);

    return 0;
}
读取管道接收数据
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>  
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#define PIPE_NAME "my.pipe"

/**
 * @brief 主函数,用于向命名管道写入数据。
 * 
 * 该程序打开一个名为"my.pipe"的命名管道,并不断从标准输入读取数据,将其写入管道中。
 * 如果打开管道失败,程序会删除管道文件并退出。
 * 
 * @return int 程序退出状态,0表示正常退出,-1表示异常退出。
 */
int main(){
    // 打开命名管道文件,以只写模式打开 参数:文件名,文件打开模式,文件权限(创建才需要)
    int fd = open(PIPE_NAME,O_WRONLY);
    if(-1 == fd) {
        // 如果打开失败,打印错误信息,删除管道文件并退出程序
        printf("打开管道失败:%m\n");
        unlink(PIPE_NAME);
        exit(-1);
    }
    printf("打开管道文件成功!\n");

    // 循环读取用户输入并写入管道
    int r;
    char buff[256];
    while(1){
        // 提示用户输入
        write(0,"你要说啥:",strlen("你要说啥:"));
        // 清空缓冲区
        memset(buff,0,256);
        // 从标准输入读取用户输入
        read(1,buff,255);
        // 将用户输入写入管道,并记录写入的字节数
        r = write(fd,buff,strlen(buff)-1);
        printf("发送%d字节数据到管道!\n",r);
    }
    return 0;
}

信号 signal

信号(Signal)​ 是 Linux 系统中进程间通信的一种机制,用于通知进程发生了某个事件。例如:

  • 用户按下 Ctrl+C(发送 SIGINT 信号终止进程)。
  • 进程执行非法操作(如除零错误触发 SIGFPE 信号)。
  • 内核通知进程资源状态变化(如子进程退出触发 SIGCHLD 信号)。

1.信号的注册与处理

不同的对象可以发送同一个信号给同一个对象。

一个对象可以接收来自不同信号源的信号。

一个对象可以发送多个信号。

一个信号只能注册一个信号处理函数。

一个信号处理函数能被多个信号注册。

常见信号列表
  • 总共64个
  • 1-32号信号是可靠信号会实时处理
  • 33-64号信号是不可靠信号
信号名默认行为说明
SIGHUP1终止进程终端连接断开(常用于通知守护进程重新加载配置)
SIGINT2终止进程用户按下 Ctrl+C(可捕获实现优雅退出)
SIGQUIT3终止+核心转储用户按下 Ctrl+,生成核心转储文件
SIGKILL9强制终止进程不可捕获或忽略(管理员最后的终止手段)
SIGTERM15终止进程默认终止信号(可捕获,用于优雅退出)
SIGSTOP19暂停进程不可捕获或忽略(暂停进程执行)
SIGCONT18继续执行恢复被暂停的进程
SIGCHLD17忽略子进程状态变化(退出或暂停)
SIGSEGV11终止+核心转储非法内存访问(如空指针解引用)
SIGUSR110终止进程用户自定义信号1(常用于自定义处理)
SIGUSR212终止进程用户自定义信号2
信号的处理方式
  1. 默认行为:由内核预定义(如终止、暂停、忽略等)。
  2. 忽略信号:明确告知内核不处理该信号。
  3. 捕获信号:自定义信号处理函数,在信号发生时执行。
信号发送 siganl
#include <signal.h>

/**
 * @brief 设置信号处理函数
 * 
 * 该函数用于设置指定信号的处理函数。当指定的信号发生时,系统将调用该处理函数。
 * 
 * @param signum 信号编号,例如 SIGINT(Ctrl+C)
 * @param handler 信号处理函数,该函数接受一个 int 类型的参数(信号编号),并返回 void
 * @return 返回之前为该信号设置的处理函数,如果之前没有设置处理函数,则返回 SIG_ERR
 */
void (*signal(int signum, void (*handler)(int)))(int);

// 示例:捕获 SIGINT(Ctrl+C)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

/**
 * @brief SIGINT 信号处理函数
 * 
 * 当接收到 SIGINT 信号(通常由 Ctrl+C 触发)时,该函数将被调用。
 * 它打印一条消息并优雅地退出程序。
 * 
 * @param sig 信号编号,此处为 SIGINT
 */
void sigint_handler(int sig) {
    printf("\n检测到ctrl+c \n");
    exit(0);
}

int main() {
    // 注册 SIGINT 信号的处理函数 (Ctrl+C)。
    signal(SIGINT, sigint_handler);

    // 主循环,保持程序运行,直到接收到 SIGINT 信号
    while (1) {
        printf("---\n");
        sleep(1);
    }
    return 0;
}
命令行工具
  • ​**kill 命令**:向指定 PID 发送信号。
    bash
    kill -SIGTERM 1234       # 发送 SIGTERM 信号
    kill -9 1234             # 强制终止进程(SIGKILL)
  • ​**pkill 命令**:按进程名发送信号。
    bash
    pkill -SIGHUP nginx      # 通知 nginx 重新加载配置
在程序中发送信号
系统调用kill()
/**
* 父进程向子进程发送 SIGUSR1
*/
    #include <unistd.h>
#include <signal.h>
#include <stdio.h>

/**
 * @brief 主函数,演示父子进程之间的信号发送与接收。
 * 
 * 该程序通过 fork() 创建一个子进程,父进程向子进程发送 SIGUSR1 信号,
 * 子进程则通过 pause() 等待信号的到来。
 * 
 * @return int 程序执行成功返回 0。
 */
int main()
{
    pid_t child_pid = fork(); // 创建子进程
    if (child_pid == 0)
    {
        // 子进程:进入无限循环,等待信号的到来
        while (1)
        {
            // 暂停进程,等待信号
            pause(); 
        }
    }
    else
    {
        // 父进程:等待 1 秒后向子进程发送 SIGUSR1 信号
        sleep(1);
        // 参数:子进程的 PID,信号类型
        kill(child_pid, SIGUSR1); // 向子进程发送 SIGUSR1 信号
        printf("发送 SIGUSR1 给子进程 %d\n", child_pid); // 打印发送信号的信息
    }
    return 0;
}
定时器信号
延时器alarm()
  • 每隔固定时间,发送一次定时器信号
/**
 * 设定 参数 多少秒 后发送一次SIGALRM信号
*/
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

/**
 * 信号处理函数,用于处理接收到的信号
 * @param s 接收到的信号编号
 */
void hand(int s){
    switch (s)
    {
    case SIGALRM:
        printf("闹钟响了!\n");
        break;
    }
}

/**
 * 主函数,设置信号处理函数并启动定时器
 * @return 无返回值
 */
int main(){
    // 设置SIGALRM信号的处理函数为hand
    signal(14,hand);

    // 设置5秒后发送SIGALRM信号
    alarm(5);

    // 无限循环,每秒打印一次进程ID和一条消息
    while(1){
        printf("我是%d,我吃着火歌唱着锅-----\n",getpid());
        sleep(1);
    }
}
定时器setitimer()
  • 每隔一端段固定时间,发送定时器信号
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>

// 定义时间结构体,表示5秒1微秒,相当于把时间搞成两位数,单独设置
struct timeval t = {5,1};

// 定义itimerval结构体,用于设置定时器
struct itimerval it;

/**
 * 信号处理函数
 * @param s 信号值
 * 该函数处理SIGALRM信号,当接收到该信号时,打印提示信息并重新设置定时器。
 */
void hand(int s){
    switch (s)
    {
    case SIGALRM:
        printf("闹钟响了!\n");
        // 设置定时器,参数1:ITIMER_REAL表示使用真实时间,参数2:it表示定时器结构体,参数3:NULL表示不返回定时器结构体。
        setitimer(ITIMER_REAL,&it,NULL);
        break;
    }
}

/**
 * 主函数
 * 该函数设置信号处理函数,初始化定时器,并进入一个无限循环,每秒打印一次进程ID和提示信息。
 */
int main(){
    // 设置SIGALRM信号的处理函数为hand
    signal(14,hand);

    // 设置定时器间隔为5秒1微秒
    it.it_value = t;

    // 启动定时器,使用ITIMER_REAL类型,表示真实时间
    setitimer(ITIMER_REAL,&it,NULL);

    // 进入无限循环,每秒打印一次进程ID和提示信息
    while(1){
        printf("我是%d,我吃着火歌唱着锅-----\n",getpid());
        sleep(1);
    }
}
自定义信号SIGUSER
  • 需要通过终端窗口 kill -SIGUSER 进程ID 才能触发信号
/**
 * 自定义信号
 * 需要通过终端窗口 kill -SIGUSER 进程ID才能触发信号
*/
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

#define SIGUSER 35  // 定义一个用户自定义信号,值为35

/**
 * @brief 信号处理函数,用于处理接收到的信号
 * 
 * @param s 接收到的信号值
 */
void hand(int s){
    switch (s)
    {
    case SIGUSER:
        printf("用户自定义信号!\n");  // 如果接收到的是用户自定义信号,打印提示信息
        break;
    }
}

/**
 * @brief 主函数,设置信号处理函数并进入无限循环
 * 
 * @return int 程序退出状态码
 */
int main(){
    signal(SIGUSER,hand);  // 设置信号处理函数,当接收到SIGUSER信号时调用hand函数

    while(1){
        printf("我是%d,我吃着火歌唱着锅-----\n",getpid());  // 打印当前进程ID
        sleep(1);  // 暂停1秒
    }
}
高级信号处理
  • 可以在接收信号同时接收信息

  • sigqueue发送信号

  • sigaction注册高级信号处理函数

高级信号发送 sigaction
  • 注册信号处理函数(支持简单处理 sa_handler 或高级处理 sa_sigaction)。
  • 控制信号处理期间的屏蔽信号。
  • 设置自动重启系统调用(SA_RESTART)或获取信号来源信息(SA_SIGINFO)。
/**
 * 通过sigaction函数设置信号处理函数,并使用sigaction函数为信号SIGINT(信号编号为2)设置新的信号处理函数。传递给sigaction函数的参数是结构体信
*/
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

/**
 * 信号处理函数,用于处理接收到的信号。
 * 
 * @param sig 接收到的信号编号
 * @param info 包含信号相关信息的结构体指针
 * @param a 未使用的上下文参数
 */
void sigactionHandler(int sig, siginfo_t* info, void* a){
    // 如果接收到的信号是SIGINT(信号编号为2),则打印发送信号的进程ID和附加的整数值
    if(2 == sig){
        printf("%d:%d\n", info->si_pid, info->si_int);
    }
}

int main(){
    // 定义并初始化sigaction结构体,用于设置信号处理函数
    struct sigaction act = {0}, oldAct;
    act.sa_sigaction = sigactionHandler;  // 设置信号处理函数
    act.sa_flags = SA_SIGINFO;  // 设置标志位,表示使用sa_sigaction作为信号处理函数

    // 使用sigaction函数为信号SIGINT(信号编号为2)设置新的信号处理函数,并保存旧的信号处理函数
    int r = sigaction(2, &act, &oldAct);
    printf("r:%d\n", r);  // 打印sigaction函数的返回值,0表示成功

    // 主循环,每秒打印一次当前进程ID和运行状态
    while(1){
        printf("%d:正常运行!\n", getpid());
        sleep(1);
    }
    return 0;
}
高级信号发送 sigqueue
  • 向指定进程发送实时信号(如 SIGRTMIN)。
  • 携带额外数据(整型或指针),供接收方信号处理函数读取。
/**
 * sigqueue发送方
*/
#include <signal.h>
#include <unistd.h>
#include <stdio.h>

/**
 * @brief 主函数,用于向指定进程发送信号并携带数据。
 * 
 * 该程序通过 `sigqueue` 函数向指定的进程发送一个实时信号(SIGRTMIN + 1),
 * 并附带一个整型数据。如果发送失败,程序将输出错误信息并返回非零值。
 * 
 * @return int 程序执行成功返回 0,失败返回 1。
 */
int main() {
    pid_t receiver_pid = 1234; // 接收方进程 PID
    union sigval value;
    value.sival_int = 42;      // 发送整型数据

    // 发送 SIGRTMIN + 1 信号并携带数据。参数1 为接收方进程 PID,参数2 为信号值,参数3 为携带的数据。
    if (sigqueue(receiver_pid, SIGRTMIN + 1, value) == -1) {
        perror("sigqueue failed");
        return 1;
    }
    printf("Signal sent with data: %d\n", value.sival_int);
    return 0;
}
/**
 * sigqueue接收方
*/
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

/**
 * 信号处理函数,用于处理接收到的实时信号。
 * 
 * @param sig 接收到的信号编号。
 * @param info 指向siginfo_t结构的指针,包含与信号相关的附加信息。
 * @param ucontext 指向ucontext_t结构的指针,包含信号发生时的上下文信息。
 */
void handler(int sig, siginfo_t *info, void *ucontext) {
    // 打印接收到的信号编号和附加数据
    printf("Received signal %d with data: %d\n", sig, info->si_value.sival_int);
}

int main() {
    struct sigaction sa;
    // 设置信号处理函数为handler
    sa.sa_sigaction = handler;
    // 设置标志位,表示使用sa_sigaction作为信号处理函数,并且提供附加信息
    sa.sa_flags = SA_SIGINFO;
    // 清空信号掩码,表示在处理当前信号时不阻塞其他信号
    sigemptyset(&sa.sa_mask);

    // 注册实时信号处理函数,处理SIGRTMIN + 1信号。参数1为信号编号,参数2为接收信号数据结构体指针,参数3为NULL
    sigaction(SIGRTMIN + 1, &sa, NULL);

    // 打印当前进程的PID,以便发送信号时使用
    printf("Receiver PID: %d\n", getpid());

    // 进入无限循环,等待信号到来
    while (1) {
        pause();
    }
    return 0;
}
信号屏蔽
sigprocmask
  • sigprocmask启动屏蔽后,会存储屏蔽期间收到的信号但不处理。解除屏蔽后再处理。(先处理最后收到的,再处理之前收到的----栈)
  • 相同的信号会覆盖。
/**
 * 信号屏蔽示例程序
 * 
 * 该程序展示了如何使用信号处理函数和信号屏蔽来控制和响应信号。
 * 程序首先设置信号处理函数,然后屏蔽指定的信号,最后解除屏蔽并进入无限循环。
 */

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

/**
 * 信号处理函数
 * 
 * 该函数用于处理接收到的信号,并根据信号类型输出相应的信息。
 * 
 * @param s 接收到的信号编号
 */
void hand(int s){
    switch (s)
    {
    case 2:
        printf("收到SIGINT!\n");
        break;
    case 3:
        printf("收到SIGQUIT--------------------!\n");
        break;
    }
}

/**
 * 主函数
 * 
 * 该函数设置信号处理函数,屏蔽指定的信号,并在10秒后解除屏蔽。
 * 
 * @param argc 命令行参数个数
 * @param argv 命令行参数数组
 * @return 程序退出状态
 */
int main(int argc,char* argv[]){
    // 设置信号处理函数
    signal(2,hand);
    signal(3,hand);

    // 初始化信号集
    sigset_t set,oldSet;
        /*
     * sigemptyset - 初始化一个空的信号集
     *
     * 该函数用于初始化一个信号集,将其设置为空集,即不包含任何信号。
     * 通常在使用信号集之前,需要先调用此函数进行初始化。
     *
     * 参数:
     *   set - 指向要初始化的信号集的指针。该信号集将被清空,不包含任何信号。
     *
     * 返回值:
     *   成功时返回0,失败时返回-1。
     */
    sigemptyset(&set);

    // 将SIGINT和SIGQUIT信号添加到信号集中
    sigaddset(&set,2);
        /**
     * sigaddset - 将指定的信号添加到信号集中
     *
     * 该函数用于将指定的信号编号添加到信号集 `set` 中。信号集通常用于进程的信号处理,
     * 通过将信号添加到信号集中,可以控制哪些信号会被阻塞或处理。
     *
     * @param set 指向信号集的指针,信号集用于存储一组信号。
     * @param signo 要添加到信号集中的信号编号。例如,3 表示 SIGQUIT 信号。
     *
     * 该函数没有返回值。
     */
    sigaddset(&set,3);

    // 检查信号是否在信号集中,并屏蔽信号
    /**
     * @brief 检查指定的信号是否在信号集中
     *
     * 该函数用于检查给定的信号是否包含在指定的信号集中。如果信号在信号集中,则返回1;否则返回0。
     *
     * @param set 指向信号集的指针,该信号集包含了要检查的信号。
     * @param signo 要检查的信号编号。
     *
     * @return 如果信号在信号集中,返回1;否则返回0。
     */
    if(sigismember(&set, 2)){
         /**
         * @brief 阻塞或解除阻塞指定的信号集
         *
         * 该函数用于修改当前进程的信号掩码,以阻塞或解除阻塞指定的信号集。
         * 通过指定不同的操作类型,可以控制信号的处理方式。
         *
         * @param SIG_BLOCK 操作类型,表示将指定的信号集添加到当前信号掩码中,从而阻塞这些信号。
         * @param &set 指向信号集的指针,该信号集包含要阻塞或解除阻塞的信号。
         * @param &oldSet 指向信号集的指针,用于保存调用前的信号掩码状态。
         *
         * @return int 返回值为0表示成功,-1表示失败,并设置errno以指示错误原因。
         */
        int r = sigprocmask(SIG_BLOCK,&set,&oldSet);
        printf("信号屏蔽:%d\n",r);
    }

    // 休眠10秒,期间信号被屏蔽
    sleep(10);

    // 检查信号是否在信号集中,并解除信号屏蔽
    if(sigismember(&set,2)){
        int r = sigprocmask(SIG_UNBLOCK,&set,&oldSet);
        printf("信号解除屏蔽:%d\n",r);
    }

    // 进入无限循环,保持程序运行
    while(1);
}

IPC

  • 操作系统给我们提供了三种进程间通信的方式。
  • 共享内存:一块内存段,同一主机上不同进程都可以访问。
  • 消息队列:同一主机上的多个进程都可以向消息队列发送各种类型的消息。也可以从消息队列中读取不同类型的消息。
  • 信号量:是一个整数,同一主机上的进程都可以加这个整数,也可以减这个整数。如果减到0就不能减了,就会阻塞,直到能继续减才解除阻塞。
  • 共享内存和消息队列都是用来传输数据的。
  • 信号量是用来控制流程的。
  • 每个ipc都有key,都有自己唯一的id

IPC相关命令

ipcs:在终端用来查看当前主机上的ipc

ipcs -q:查看消息队列 ipcs -s:查看信号量 ipcs -m:查看共享内存

ipcrm:在终端用来删除当前主机上的某个ipc

ipcrm -q 65536:删除消息队列 ipcrm -s 65536:删除信号量 ipcrm -m 65536:删除共享内存

ftok 创建IPC键值

ftok用于将一个文件路径(path)和一个项目 ID转换为一个唯一的 key_t键值,该键值用于创建或访问IPC 对象(如消息队列、信号量、共享内存)

/**
 * 生成键值并创建共享内存
 * 
 * 该程序通过指定文件路径和项目ID生成一个唯一的键值,并使用该键值创建一块共享内存。
 * 共享内存的大小为1024字节,权限设置为0666(所有用户可读写)。
 * 
 * @return int 返回值为0表示程序成功执行,非0表示出现错误。
 */
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    const char *path = "/tmp/myfile"; // 确保该文件存在
    int proj_id = 42;                // 项目 ID 范围:1-255

    // 生成键值 参数1:文件路径 参数2:项目 ID
    key_t key = ftok(path, proj_id);
    if (key == -1) {
        perror("ftok failed");
        exit(EXIT_FAILURE);
    }

    // 创建共享内存 参数1:键值 参数2:共享内存大小 参数3:共享内存权限
    int shmid = shmget(key, 1024, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget failed");
        exit(EXIT_FAILURE);
    }

    printf("Shared memory created with key: 0x%x\n", key);
    return 0;
}
共享内存 shmget
/**
 * 创建共享内存并从共享内存中读取数据并打印
 *
 * 该程序通过以下步骤实现共享内存的创建和数据的读取:
 * 1. 使用ftok函数生成一个唯一的key,用于标识共享内存。
 * 2. 使用shmget函数创建共享内存段,并指定大小和权限。
 * 3. 使用shmat函数将共享内存段挂载到当前进程的地址空间。
 * 4. 在一个无限循环中,不断读取共享内存中的数据并打印。
 *
 * 如果任何步骤失败,程序将打印错误信息并退出。
 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main()
{
    // 使用ftok函数生成一个唯一的key,用于标识共享内存。参数1为文件路径,参数2为文件路径下的一个字符,用于标识共享内存。
    key_t key = ftok(".", 100);
    if (-1 == key)
        printf("创建key失败:%m\n"), exit(-1);
    printf("创建key成功!\n");

    
        /**
     * 使用shmget函数创建一个共享内存段。
     * 
     * @param key 共享内存段的键值,通常使用ftok函数生成,用于唯一标识共享内存段。
     * @param size 共享内存段的大小,单位为字节。此处设置为4字节。
     * @param shmflg 标志位,用于指定共享内存段的创建方式和权限。
     *              IPC_CREAT: 如果共享内存段不存在,则创建它。
     *              0666: 设置共享内存段的权限为可读可写,所有用户均可访问。
     * 
     * @return 成功时返回共享内存段的标识符(shmid),失败时返回-1。
     */
    int shmid = shmget(key, 4, IPC_CREAT | 0666);
    if (-1 == shmid)
        printf("创建shm失败:%m\n"), exit(-1);
    printf("创建shm %m\n");
        /*
     * 函数:shmat
     * 功能:将共享内存段附加到调用进程的地址空间
     * 参数:
     *   - shmid: 共享内存标识符,由shmget函数返回
     *   - shmaddr: 指定共享内存附加到进程地址空间的位置,NULL表示由系统选择合适的位置
     *   - shmflg: 附加标志,0表示默认行为
     * 返回值:
     *   - 成功时返回指向共享内存段的指针
     *   - 失败时返回(void *)-1,并设置errno
     */
    int *p = shmat(shmid, NULL, 0);
    if ((int *)-1 == p)
    {
        printf("挂载失败:%m\n");
         /**
         * shmctl - 控制共享内存段
         *
         * 该函数用于对指定的共享内存段执行控制操作。通过不同的命令参数,可以执行删除、获取信息等操作。
         *
         * @param shmid 共享内存段的标识符。该标识符通常由shmget函数创建。
         * @param IPC_RMID 命令参数,表示删除共享内存段。当使用此命令时,共享内存段将被标记为删除,
         *                 并且在所有进程都断开连接后,该内存段将被系统自动释放。
         * @param NULL 用于指定共享内存段的信息结构体指针。当使用IPC_RMID命令时,此参数通常为NULL,
         *             因为不需要传递额外的信息。
         *
         * @return 成功时返回0,失败时返回-1,并设置errno以指示错误类型。
         */
        shmctl(shmid, IPC_RMID, NULL);
        exit(-1);
    }
    printf("挂载成功:%p\n", p);

    // 在一个无限循环中,不断读取共享内存中的数据并打印
    while (1)
    {
        printf("*p:%d\n", *p);
        sleep(1);
    }

    return 0;
}
/**
 * 挂载到共享内存并每隔1s写入一个整数到共享内存中。
 * 
 * 该程序通过以下步骤实现功能:
 * 1. 创建一个唯一的key用于标识共享内存。
 * 2. 使用该key创建共享内存段。
 * 3. 将共享内存段挂载到当前进程的地址空间。
 * 4. 每隔1秒向共享内存中写入一个递增的整数。
 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main(){
    // 创建key,用于标识共享内存
    key_t key = ftok(".",100);
    if(-1 == key) printf("创建key失败:%m\n"),exit(-1);
    printf("创建key成功!\n");

    // 创建共享内存段,大小为4字节,权限为0666
    int shmid = shmget(key,4,IPC_CREAT|0666);
    if(-1 == shmid) printf("创建shm失败:%m\n"),exit(-1);
    printf("创建shm %m\n");

    // 将共享内存段挂载到当前进程的地址空间
    int* p = shmat(shmid,NULL,0);
    if((int*)-1 == p) printf("挂载失败:%m\n"),shmctl(shmid,IPC_RMID,NULL),exit(-1);
    printf("挂载成功:%p\n",p);

    // 每隔1秒向共享内存中写入一个递增的整数
    int n=0;
    while(1){
        *p = n++;
        sleep(1);
    }

    return 0;
}
消息队列 msgget
/**
 * 用消息队列发送消息
 * 
 * 该程序演示了如何使用消息队列发送消息。程序首先创建一个唯一的key,然后使用该key创建或获取一个消息队列。
 * 接着,程序准备一组消息,并通过消息队列发送这些消息。
 * 
 * 返回值:无
 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

/**
 * @struct msgbuf
 * @brief 消息缓冲区结构体,用于存储消息类型和消息内容。
 *
 * 该结构体通常用于进程间通信(IPC)中的消息队列,包含一个长整型的消息类型和一个字符数组的消息内容。
 *
 * @var msgbuf::mtype
 * 消息类型,用于标识消息的类别或优先级。接收方可以根据消息类型选择性地接收消息。
 *
 * @var msgbuf::mtext
 * 消息内容,用于存储实际的消息数据。该数组的大小为256字节,可以存储较短的文本消息。
 */
struct msgbuf{
    long mtype;
    char mtext[256];
};

int main(){
    // 创建key,用于标识消息队列
    key_t key = ftok(".",200);
    if(-1 == key) printf("创建key失败:%m\n"),exit(-1);
    printf("创建key成功!\n");

    // 创建或获取消息队列
     /**
     * 使用msgget函数创建一个消息队列或获取一个已存在的消息队列的标识符。
     *
     * @param key 用于标识消息队列的键值。通常使用ftok函数生成,或者使用IPC_PRIVATE创建一个唯一的消息队列。
     * @param IPC_CREAT 标志位,表示如果消息队列不存在则创建它。
     * @param 0666 权限标志,表示消息队列的访问权限。0666表示所有用户都有读写权限。
     * @return 成功时返回消息队列的标识符(msgid),失败时返回-1,并设置errno以指示错误类型。
     */
    int msgid = msgget(key, IPC_CREAT | 0666);
    if(-1 == msgid) printf("创建msg失败:%m\n"),exit(-1);
    printf("创建msg %m\n");

    // 准备一组消息,每条消息包含类型和内容
    struct msgbuf buff[5] = {
        {1,"李澳"},
        {2,"何家鑫"},
        {1,"李健"},
        {2,"邓良斌"},
        {2,"陈云龙"}
    };

    // 循环发送消息到消息队列
    for(int i=0;i<5;i++){
                /*
         * 函数:msgsnd
         * 功能:向消息队列发送消息
         * 参数:
         *   - msgid: 消息队列的标识符,用于指定要发送消息的队列
         *   - buff+i: 指向消息缓冲区的指针,buff 是消息数组,i 是数组索引,表示要发送的消息
         *   - strlen(buff[i].mtext): 消息文本的长度,计算的是 buff[i].mtext 的长度
         *   - IPC_NOWAIT: 发送消息的标志,如果消息队列已满,立即返回而不等待
         * 返回值:
         *   - 成功时返回 0,失败时返回 -1 并设置 errno
         */
        int r = msgsnd(msgid, buff+i, strlen(buff[i].mtext), IPC_NOWAIT);
        printf("发送第%d条消息%d\n",i,r);
    }
}
/**
 * 用消息队列接收消息
 * 
 * 该程序通过消息队列接收消息,并打印接收到的消息内容。
 * 程序首先创建一个唯一的key,然后使用该key创建或获取一个消息队列。
 * 接着,程序准备一个消息缓冲区,并从消息队列中接收消息,最后打印接收到的消息内容。
 */

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

/**
 * 消息结构体
 * 
 * @param mtype 消息类型,用于标识消息的类别
 * @param mtext 消息内容,存储实际的消息数据
 */
struct msgbuf{
    long mtype;
    char mtext[256];
};

int main(){
    // 创建唯一的key,用于标识消息队列
    key_t key = ftok(".",200);
    if(-1 == key) printf("创建key失败:%m\n"),exit(-1);
    printf("创建key成功!\n");

    // 使用key创建或获取一个消息队列
    int msgid = msgget(key,IPC_CREAT|0666);
    if(-1 == msgid) printf("创建msg失败:%m\n"),exit(-1);
    printf("创建msg %m\n");

    // 准备一个消息缓冲区,用于存储接收到的消息
    struct msgbuf buff[5] = {0};

    // 从消息队列中接收消息,并存储到缓冲区中
    for(int i=0;i<5;i++){
         /**
         * 从消息队列中接收消息。
         * 
         * 该函数从指定的消息队列中接收一条消息,并将其存储在提供的缓冲区中。
         * 
         * @param msgid 消息队列的标识符,用于指定从哪个消息队列接收消息。
         * @param buff+i 接收消息的缓冲区地址,`i` 表示缓冲区的偏移量,用于指定消息存储的位置。
         * @param 255 接收消息的最大字节数,表示从消息队列中接收的消息的最大长度。
         * @param 2 接收消息的类型,表示只接收类型为2的消息。
         * @param IPC_NOWAIT 接收消息的标志,表示如果没有符合条件的消息,函数立即返回而不等待。
         * 
         * @return 成功时返回接收到的消息的字节数,失败时返回-1,并设置errno以指示错误。
         */
        int r = msgrcv(msgid, buff+i, 255, 2, IPC_NOWAIT);
        printf("接收第%d条消息%d\n",i,r);
    }

    // 打印接收到的消息内容
    for(int i=0;i<5;i++){
        printf("%d:%s\n",i,buff[i].mtext);
    }
}
信号量(旗语) semget
/**
 * 信号量发送和接收消息
 * 该程序演示了如何使用信号量进行进程间通信。程序首先创建了一个信号量,并设置其初始值,
 * 然后通过信号量操作进行消息的发送和接收。
 */

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <string.h>

/*
 * union semun
 * 信号量联合体,用于不同的信号量操作
 * 该联合体用于System V信号量操作中的semctl函数,提供了不同的数据结构来支持不同的操作。
 * 
 * 成员说明:
 * - val: 用于SETVAL操作,指定信号量的值。
 * - buf: 指向semid_ds结构的指针,用于IPC_STAT和IPC_SET操作,用于获取或设置信号量集合的状态信息。
 * - array: 指向unsigned short数组的指针,用于GETALL和SETALL操作,用于获取或设置所有信号量的值。
 * - __buf: 指向seminfo结构的指针,用于IPC_INFO操作,获取信号量系统的信息(Linux特有)。
 */
union semun {
    int              val;    /* 用于SETVAL的值 */
    struct semid_ds *buf;    /* 用于IPC_STAT, IPC_SET的缓冲区 */
    unsigned short  *array;  /* 用于GETALL, SETALL的数组 */
    struct seminfo  *__buf;  /* 用于IPC_INFO的缓冲区 (Linux特有) */
};

int main(){
    // 1. 创建key,用于标识信号量
    key_t key = ftok(".",111);
    if(-1 == key) printf("创建key失败:%m\n"),exit(-1);
    printf("创建key成功!\n");
 
    // 2. 创建信号量,使用IPC_CREAT标志创建信号量集,并设置权限为0666
        /**
     * 创建一个信号量集或获取已存在的信号量集的标识符。
     *
     * @param key 用于标识信号量集的键值。通常使用ftok函数生成。
     * @param nsems 信号量集中信号量的数量。此处设置为1,表示创建一个包含单个信号量的信号量集。
     * @param semflg 创建标志和权限模式的组合。IPC_CREAT表示如果信号量集不存在则创建它,0666表示赋予所有用户读写权限。
     * @return 成功时返回信号量集的标识符(semid),失败时返回-1并设置errno。
     */
    int semid = semget(key,1,IPC_CREAT|0666);
    if(-1 == semid) printf("创建信号量失败:%m\n"),exit(-1);
    printf("创建信号量%m\n");

    // 3. 设置信号量初始值为5
    union semun su;
    su.val = 5;
    
        /**
     * 使用semctl函数对指定的信号量进行操作。
     * 
     * 该函数调用semctl来设置信号量集中某个特定信号量的值。
     * 
     * @param semid 信号量集的标识符,用于唯一标识一个信号量集。
     * @param 2 信号量在信号量集中的索引,表示要操作的是信号量集中的第3个信号量(索引从0开始)。
     * @param SETVAL 操作命令,表示要设置信号量的值。
     * @param &su 指向semun联合体的指针,其中包含要设置的值。
     * 
     * @return int 返回semctl函数的执行结果。成功时返回0,失败时返回-1并设置errno。
     */
    int r = semctl(semid,2,SETVAL,&su);
    printf("semctl:%d %m\n",r);

    // 获取信号量状态信息并打印
    struct semid_ds ds;
    union semun semUn;
    semUn.buf = &ds;
    r = semctl(semid,0,IPC_STAT,&semUn);
    
    printf("r:%d,sem_nsems:%d,uid:%o,mode:%o\n",
        r,
        ds.sem_nsems,
        ds.sem_perm.uid,
        ds.sem_perm.mode);

    // 4. 操作信号量,先将其值设置为5
    struct sembuf sop = {0};
    sop.sem_num = 0;
    sop.sem_op = 5;
    sop.sem_flg = IPC_NOWAIT;

    semop(semid,&sop,1);

    // 再次获取信号量状态信息并打印
    r = semctl(semid,0,IPC_STAT,&semUn);
    
    printf("r:%d,sem_nsems:%d,uid:%o,mode:%o\n",
        r,
        ds.sem_nsems,
        ds.sem_perm.uid,
        ds.sem_perm.mode);

    // 将信号量值减1,模拟消息的接收操作
    sop.sem_op = -1;
    printf("开始操作!\n");
    int n = 0;
    while(1){
                /*
         * 函数: semop
         * 功能: 对指定的信号量集合执行一个或多个信号量操作。
         * 参数:
         *   - semid: 信号量集合的标识符,通常由semget函数返回。
         *   - sop: 指向sembuf结构体的指针,该结构体定义了要执行的操作。
         *   - nsops: 要执行的操作数量,通常为1,表示只执行一个操作。
         * 返回值:
         *   - 成功时返回0,失败时返回-1,并设置errno以指示错误。
         */
        r = semop(semid, &sop, 1);
        if(-1 != r)
            printf("操作成功:%d\n",n++);
    }

    return 0;
}
/**
 * 
 * 该程序演示了如何使用信号量进行进程间通信。程序首先创建一个唯一的key,
 * 然后使用该key创建一个信号量集。接着,程序进入一个无限循环,不断尝试对信号量进行操作。
 * 
 * 返回值:
 *   - 程序正常结束时返回0。
 *   - 如果创建key或信号量失败,程序将打印错误信息并退出,返回-1。
 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <string.h>

/**
 * 信号量联合体
 * 
 * 用于在信号量操作中传递不同的参数类型。
 */
union semun {
    int              val;    /* 用于SETVAL的值 */
    struct semid_ds *buf;    /* 用于IPC_STAT和IPC_SET的缓冲区 */
    unsigned short  *array;  /* 用于GETALL和SETALL的数组 */
    struct seminfo  *__buf;  /* 用于IPC_INFO的缓冲区(Linux特有) */
};

int main(){
    // 1. 创建key
    key_t key = ftok(".",111);
    if(-1 == key) printf("创建key失败:%m\n"),exit(-1);
    printf("创建key成功!\n");
 
    // 2. 创建信号量
    int semid = semget(key,1,IPC_CREAT|0666);
    if(-1 == semid) printf("创建信号量失败:%m\n"),exit(-1);
    printf("创建信号量%m\n");

    // 3. 操作信号量
    struct sembuf sop = {0};
    sop.sem_num = 0;  // 信号量编号
    sop.sem_op = 2;   // 操作值,表示增加信号量的值
    sop.sem_flg = IPC_NOWAIT;  // 非阻塞模式

    printf("开始操作!\n");
    while(1){
        int r = semop(semid,&sop,1);  // 执行信号量操作
        printf("r :%d\n",r);
        sleep(1);
    }
    return 0;
}
/**
 * 信号量实现控制
 * 该程序通过信号量机制控制一个计数器的增加和减少,当计数器达到100时,重置计数器并模拟“卖掉100件”的操作。
 */

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <string.h>

int semid; // 信号量ID

/**
 * myPlus - 减少信号量的值
 * 
 * 该函数尝试将信号量的值减少1。如果信号量的值已经为0,则立即返回失败。
 * 
 * @return 成功返回0,失败返回-1。
 */
int myPlus() {
    struct sembuf sop = {0};
    sop.sem_num = 0; // 信号量编号
    sop.sem_op = -1; // 操作:减少信号量值
    sop.sem_flg = IPC_NOWAIT; // 非阻塞模式

    return semop(semid, &sop, 1); // 执行信号量操作
}

int main() {
    // 1. 创建key
    key_t key = ftok(".", 123); // 使用当前目录和项目ID生成key
    if (-1 == key) {
        printf("创建key失败:%m\n");
        exit(-1);
    }
    printf("创建key成功!\n");

    // 2. 创建信号量
    semid = semget(key, 1, IPC_CREAT | 0666); // 创建一个信号量集,权限为0666
    if (-1 == semid) {
        printf("创建信号量失败:%m\n");
        exit(-1);
    }
    printf("创建信号量%m\n");

    // 初始化信号量值为20
    struct sembuf sop = {0};
    sop.sem_num = 0; // 信号量编号
    sop.sem_op = 20; // 操作:增加信号量值
    sop.sem_flg = IPC_NOWAIT; // 非阻塞模式
    semop(semid, &sop, 1); // 执行信号量操作

    int num = 0; // 计数器

    // 3. 操作信号量
    while (1) {
        // 尝试减少信号量值,如果成功则增加计数器
        if (!myPlus()) {
            num += 5;
            printf("当前num:%d\n", num);
        }

        // 当计数器达到100时,重置计数器并模拟“卖掉100件”的操作
        if (num >= 100) {
            num = 0;
            semop(semid, &sop, 1); // 重置信号量值
            printf("卖掉100件----------\n");
            sleep(1); // 模拟操作延迟
        }
    }

    return 0;
}
守护进程
/**
 * 守护进程
 * 该函数用于创建一个守护进程。守护进程是一种在后台运行的进程,通常用于执行系统任务或服务。
 * 该函数通过fork()创建子进程,父进程退出,子进程成为新的会话组长,并脱离终端控制。
 * 子进程会改变工作目录、解除文件屏蔽、屏蔽信号,并关闭标准输入、输出和错误文件描述符。
 * 守护进程进入无限循环,通常用于执行后台任务,如记录日志等。
 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>

int main(void)
{
    pid_t pid = 0;
    int i = 0;
    pid = fork();

    if(pid > 0){
        // 父进程打印进程ID并退出,确保子进程成为孤儿进程
        printf("pid : %d\n", getpid());
        exit(0);
    }else if(pid == 0){
        // 子进程成为新的会话组长,脱离终端控制
                /**
         * setsid - 创建一个新的会话并设置调用进程为会话组长
         *
         * 该函数使调用进程成为一个新会话的组长,并且脱离当前的控制终端。调用进程将成为新会话的唯一进程,
         * 并且成为新进程组的组长。调用进程的进程组ID将被设置为调用进程的进程ID。
         *
         * 返回值:
         *   - 成功时,返回新会话的会话ID(即调用进程的进程ID)。
         *   - 失败时,返回-1,并设置errno以指示错误类型。
         */
         setsid();
        
        // 打印当前子进程ID
        printf("pid : %d\n", getpid());   
       
        // 解除文件屏蔽,确保子进程创建的文件具有所有权限
        /*
         * 函数:umask
         * 功能:设置文件模式创建屏蔽字(umask),并返回之前的umask值。
         * 参数:mode_t mask - 新的文件模式创建屏蔽字。通常是一个八进制数,表示在创建文件时需要屏蔽的权限位。
         * 返回值:返回之前的umask值。
         * 说明:umask函数用于设置进程的文件模式创建屏蔽字,该屏蔽字用于在创建新文件或目录时屏蔽掉指定的权限位。
         *       例如,umask(0)表示不屏蔽任何权限位,新创建的文件或目录将具有调用者指定的完整权限。
         */
        umask(0);
        // 改变当前工作目录到根目录,避免占用挂载点
        chdir("/");

        // 屏蔽SIGINT信号,防止用户通过Ctrl+C终止守护进程
        /**
         * @brief 设置信号处理方式
         * 
         * 该函数调用 `signal` 函数,用于设置指定信号的处理方式。
         * 
         * @param 2 信号编号,表示要设置的信号。在此代码中,信号编号为 2,通常对应 SIGINT 信号(即用户按下 Ctrl+C 时产生的信号)。
         * @param SIG_IGN 信号处理方式,表示忽略该信号。在此代码中,SIG_IGN 表示忽略信号编号为 2 的信号。
         * 
         * @return 无返回值。
         */
        signal(2,SIG_IGN);

        // 关闭标准输入、输出和错误文件描述符,防止守护进程与终端交互
        close(STDIN_FILENO);
        close(STDOUT_FILENO);
        close(STDERR_FILENO);     
        
        // 守护进程进入无限循环,通常用于执行后台任务
        while( 1 )
        {
            // 守护进程正常的工作内容,如记录日志等
        }
    }

    return 0;
}

静态库和动态库

  • 函数库(面向过程)
  • 类库(面向对象)

两种方式调用库中代码

静态调用(静态库)
  • 链接的时候把代码放入到可执行文件中
  • 编译链接时间相对较长
  • 可执行文件相对较大
  • 不太灵活,如果改代码了,需要停机
动态调用(动态库)
  • 运行时加载,运行的时候再把代码拿过来(类型场景,驱动程序安装)
  • 编译链接时间短
  • 可执行文件相对较小
  • 灵活无需停机

gcc的使用

-o选项

指定生成文件名

gcc *.c

直接编译生成可执行程序文件

gcc *.c 生成a.out

gcc *.c -o a.exe 生成a.exe

gcc -E *.c

仅预处理

直接输出到终端,没有生成文件

gcc -E *.c > a.txt 就可以打开a.txt查看

gcc -c *.c

仅编译

gcc -c main.c 生成main.o

gcc -c main.c -o MAIN.o 生成MAIN.o

gcc -s *.c

编译成汇编

gcc -s main.c 生成 main.s

gcc -s main.c -o a.s 生成a.s

静态库的创建和使用

创建静态库

代码功能其他
ar -crs libDemo.a Demo.o将编译后的二进制文件Demo.o打包成静态库libDemo.a静态库名称必须以lib开头
gcc main.o -L . -l libDemogcc编译文件时使用当前路径下的静态库libDemo-L指定静态库路径,.是当前路径,-l是指定静态库名称
gcc math_demo.c -lm -o math_demogcc编译文件时使用环境变量下的静态库-lm链接环境变量的静态库

创建动态态库

代码功能其他
ar -shared libDemo.so Demo.o将编译后的二进制文件Demo.o打包成动态库libDemo.so动态库名称必须以lib开头
gcc -c -fPIC add.c将要编译成动态库前的二进制文件打包-fPIC这个指令不加,后续使用动态库的时候会报错
ar -shared libDemo.so Demo.o将编译后的二进制文件Demo.o打包成动态库libDemo.so动态库名称必须以lib开头
gcc main.o -L . libDemogcc编译文件时使用当前路径下的动态库libDemo-L指定动态库路径,.是当前路径,这里调试不一定生效,可能需要配置过环境变量的路径才可以
环境变量
  • 环境变量是指一些特殊路径,系统知道这个路径,这个路径下的可执行文件可以在任何目录运行,不需要加路径名了
  • 使用动态库的时候,只会去看环境变量中设置的路径,如果环境变量中设置的路径里没有动态库文件,就会提示找不到
  • #include "" 当前目录下找
  • #include <> 去指定路径下找,这个指定路径就在环境变量中设置
  • linux也可以设置环境变量,在一个文件中

线程

线程是什么

  • 线程是操作系统调度的基本单位
  • 从流程角度看,线程是基本单位,考虑资源的话,进程是基本单元
  • 一个进程内可以有多个线程,至少有一个线程(主线程main函数)
  • 同一个线程共享进程资源

如何启动线程

  • 多个线程可以共用一个线程函数
  • 依赖头文件 <pthread.h>
  • gcc时,后面需加-l pthread
代码功能参数返回值其他
pthread_t pt创建线程id
int p= pthread_create(&pt,NULL,f,NULL);创建子线程线程id,线程属性结构体(一般写NULL),线程函数地址,线程函数参数0成功,-1失败f回调函数参数类型只能传void*,可以用取地址、解引用等方式传别的数据类型
pthread_join(pt,&ret)在主线程等待子线程结束并回收其资源参数:要等待的线程标识符,指向指针的指针用于接收线程的返回值(如果线程有返回值)。可设为 NULL成功返回 0,失败返回错误码(非零值)第二个参数可以知道子线程是否结束
pthread_exit(n)在子线程回调内显示终止当前线程参数:线程的返回值(可以是任意类型的指针)。主线程可通过 pthread_join 的第二个参数获取此值无返回值在子线程回调函数中可以在某些条件下主动通过该函数结束子线程,并将返回值传出去
pthread_cancel(pt)在主线程通过线程id终止子线程参数:线程id无返回值不止在主线程可以干掉,在其他子线程也可以通过子线程id干掉

线程参数

  • 线程回调函数只能传递一个void*类型的参数,通过指针参数的形式,基本上所有类型的数据都可以作为参数传递进去
  • 传递参数都需要使用&取地址
  • 打印和使用参数时,类型需要强转(int *)p
  • 还可以通过全局变量的方式,使用外面的变量

线程结束

  1. main函数结束、ruturn 0;
  2. 没有代码了,省略了return
  3. exit(-1);
  4. 终端关闭
  5. 通过信号结束
  6. 主线程结束会导致分支线程结束,而分支线程结束不会导致其创建的孙线程结束。
通过pthread_join等待分支线程结束
代码功能参数返回值其他
pthread_join(pt,&ret)等待指定线程结束并回收其资源参数:要等待的线程标识符,指向指针的指针用于接收线程的返回值(如果线程有返回值)。可设为 NULL成功返回 0,失败返回错误码(非零值)第二个参数可以知道子线程是否结束

pthread_join第二个参数在线程结束前会自动申请内存,不需要在主线程中申请内存,结束后该变量会拿到子线程回调的返回值

线程同步

当一个主线程,在里面创建两个及以上子线程,然后传给子线程的回调函数,使用同一个的话,就形成了一个多个线程共用的区域,叫临界区 临界区是同一段内存,里面定义的代码和变量这些,各个子线程都能访问,我们称之为临界数据 如果各个子线程同时去修改这个回调函数里的变量的话,数据就会很乱,到底谁改成功就不可预测,这种情况称之为临界数据脏。 为了解决临界数据脏的问题,使用一些方式或规则让多个线程互斥也就是不能同时去访问临界区,让异步执行的线程不会同时去改变变量,这些方式或规则称之为线程同步

临界数据

个人理解,多个子线程,使用同一个回调函数,该回调函数的作用域内的变量,就是临界数据

  • 多个线程可以同时访问的区域叫做临界区(线程共用的回调函数)。
  • 多个线程可以同时访问的数据我们称之为临界数据(线程共用的回调函数里的变量)。
  • 多个线程同时写访问临时数据,可能导致临界数据脏(不同的子线程都去改某个变量)。
  • 为了解决临界数据脏的问题,使用线程同步的方式让多个线程有顺序的去访问临界区,让异步执行的线程,通过等待的方式不会同时去改变变量

线程同步分类

用户态
  • 操作系统在用户状态下执行,不需要执行内核代码,锁的开销相对较小
  • 例:互斥锁、读写锁、条件变量
内核态
  • 操作系统需要切换到内核态执行,所以存在操作系统状态的切换,锁的开销相对较大
  • 例:原子锁、信号量、自旋锁

线程锁

原子锁 atomic

  • linux3.0.8以后版本无法使用
  • 具备原子特性
  • 主要用在数字等操作
  • 操作简单,只能执行简单的业务逻辑
  • 依赖 stdatomic.h头文件

互斥锁 mutex

  • 原理类似快递柜,只有等上一个人的快递拿出来了,下一个人的快递才能放进去
  • 具体操作为访问临界区域前,加锁,让其他线程无法访问。访问完毕后,解锁,其他线程就可以访问了。
  • 注意不要造成死锁
  • 互斥锁是不公平的
代码功能参数返回值其他
pread_mutex_t mutex; 定义互斥量
pread_mutex_init(&mutex,NULL); 初始化互斥锁互斥量取地址,锁的属性(可以为NULL)成功返回0,非0表示错误
int r = pthread_mutex_trylock(&mutex); 尝试是否已上锁互斥量取地址0上锁成功,EBUSY表示已上锁trylock会尝试去上锁,如果能上锁,就会锁住
int r = pthread_mutex_lock(&mutex); 阻塞式锁上锁互斥量取地址0上锁成功,EBUSY表示已上锁如果未上锁会直接上锁,已上锁会阻塞线程,直到锁释放
int r = pthread_mutex_destroy(&mutex); 销毁锁互斥量取地址0销毁成功,其他表示销毁失败
sched_yield(); 当前线程让出cpu

读写锁 rwlock

  • 原理是有两把锁,一把读锁、一把写锁
  • 读读相容,读写,写写相斥,
代码功能参数返回值其他
pread_rwlock_t rwlock; 定义读写锁
pthread_rwlock_init(&rwlock, NULL); 初始化读写锁读写锁取地址,锁的属性
pthread_rwlock_rdlock(&rwlock); 阻塞式加读锁成功返回0失败了会尝试再次加锁
pthread_rwlock_tryrdlock(&rwlock); 非阻塞式加读锁成功返回0失败了不会尝试再次加锁
pthread_rwlock_trywrlock(&rwlock); 非阻塞式加写锁成功返回0失败了不会尝试再次加锁
pthread_rwlock_wrlock(&rwlock); 阻塞式加写锁成功返回0失败了会尝试再次加锁
pthread_rwlock_unlock(&rwlock); 解锁
int pthread_rwlock_destroy(&rwlock);销毁读写锁要销毁的读写锁指针成功返回0

条件变量 cond

  • 也称之为事件
  • 例如:某个地方发生了一件很奇怪的事,就有很多记者来采访,当事人只能接受一个人的专访,所以就从众多记者里选一位进行专访。如果记者们还想采访,就要等另一件事情发生。
  • 使用pthread_cond_wait来让线程阻塞
  • 另外的线程使用pthread_cond_signal(单发)或者pthread_cond_bordcast(群发)来产生事件。
  • 调用了pthread_cond_wait的线程会收到信号,收到信号后解除阻塞
代码功能参数返回值其他
pread_cond_t cond; 定义条件变量
pthread_cond_init(&cond,NULL); 初始化条件变量
pthread_cond_wait(&cond, &mutex);释放互斥锁并阻塞线程,直到条件变量被唤醒指向条件变量的指针,指向关联的互斥锁的指针(调用前必须已加锁)成功返回0,失败返回错误码等待某个条件成立,需配合循环检查条件以防止虚假唤醒
pthread_cond_timedwait(&cond, &mutex, &ts);指定时间内等待条件变量,超时后返回。指向条件变量的指针,指向关联的互斥锁的指针,绝对超时时间成功返回0,超时返回ETIMEDOUT
pthread_cond_signal(&cond);唤醒至少一个等待该条件变量的线程指向条件变量的指针成功返回0,失败返回错误码
pthread_cond_broadcast(&cond);唤醒所有等待该条件变量的线程指向条件变量的指针成功返回0,失败返回错误码
pthread_cond_destroy(&cond);销毁条件变量

粒度

  • 和行业黑话"对齐颗粒度"的颗粒度是一个意思。
  • 假设需要加锁的代码有100行。那么有很多种方式。
  • 情况一:加一把锁,颗粒度最大。
  • 锁的开销很小,其他线程等待时间很长,并发感受弱。
  • 情况二:加100把锁,颗粒度最小。
  • 锁的开销很大,其他线程等待时间最短。
  • 我们需要根据实际情况来设计锁住区域的大小(粒度)。

自旋锁 spin

  • 自旋锁顾名思义就是有一个循环(自旋)在不断执行,尝试去加锁。一旦被其得逞。其他的锁就加不上。
  • 就是轮询模式:不断去尝试,资源开销相对大,实时性强
  • 互斥锁就是中断模式:等待通知,资源开销相对小,实时性相对弱。
  • 自旋锁一般不建议锁住一块很大的区域。
代码功能参数返回值其他
pread_spinlock_t spin; 定义自旋锁
pthread_spin_init(&spin,PTHREAD_PROCESS_PRIVATE); 初始化自旋锁自旋锁指针,共享属性0成功,失败返回错误码共享属性:0仅同一线程可共享,PTHREAD_PRCESS_SHARED进程间共享
pthread_spin_lock(&spinlock)阻塞加锁
pthread_spin_trylock(&spinlock);非阻塞加锁
pthread_spin_unlock(&spinlock);解锁
pthread_spin_destroy(&spinlock);销毁自旋锁

死锁

  • 死锁一旦产生,就无法解除;所以要避免其产生。
  • 必须同时达成以下四个条件才会死锁:
  1. 请求并保持
  2. 持续请求(不可剥夺)
  3. 不可摧毁
  4. 不可撤销

线程池

  • 池化技术的一种。就像容器一样动态管理多个线程,供程序使用。
  • 三个线程:
  1. 线程一:专门负责封装任务,并把任务存放到任务池中。
  2. 线程二:从任务池中取出任务,交给线程池中的某个线程去处理
  3. 线程三:监督线程池,如果池中空闲线程过多,就删掉一些;如果池中都是工作线程,没有空闲线程可用,就创建一些。实时调整线程状态(某个工作线程任务完成了,调整其状态为空闲线程)。

网络编程

什么叫网络

网络分层

五层网络
  • 最上层和人接触
  • 最下层和硬件接触
  • 应用层:用户交互、https、ftp
  • 传输层:路由器、tcp、udp
  • 网络层:交换机
  • 数据链路层:数模转换、网卡
  • 物理层:无线电、双绞线、光纤
七层网络
  • 应用层:
  • 表示层:
  • 会话层:
  • 传输层:
  • 网络层:
  • 数据链路层:
  • 物理层:
网络常识
  • mac地址:物理地址,主机出厂就设定就唯一的,和网卡绑定
  • ip地址:网络中每台主机分配一个唯一无符号整数(网络字节序),用来区分
  • ipv4协议:4个字节 32个bit位 "192.168.10.65"(可以将字符串转为网络字节序)
  • ipv6协议: 20个字符 20个字节 2^160个bit位 "fe80::a5cc:63f5:3b52:7b19%10"
  • 网络端口:网卡为主机上每个进程分配网络io口,一个进程需要绑定唯一一个网络端口。操作系统35536个端口
  • 特殊端口:21号端口,teinet、ssh
  • 80号端口,浏览器使用
  • 开发代码,一般用10000号端口
  • 大小端问题
  • 大端:高位在低地址,低位在高地址,arm架构
  • 小端:高位在高地址,低位在低地址,x86架构
  • 子网掩码:是为了截取32个位中的某些位,255.255.255.0,它&任意一个数,得到高位24位不变,低8位清零。
  • 一般高24位用来区分路由器(某个局域网),低8位用来区分路由器上某个主机,默认网关一般是路由器中1号主机
协议
  • 约定俗成的各种步骤。
  • 公有协议:大部分公式和个人约定的,国标
  • 私有协议: 小公司和个人约定的,非标

tcp协议

CS架构

  • 中心化:以服务器为中心
  • 服务器Server:所有功能都由服务器来实现
  • 客户端Client:和用户交互界面
  • tcp协议,客户端链接服务器之后会建立稳定的数据传输通道(全双工)
  • 全双工:两端同时收发,例如共享内存
  • 半双工:一端收,另一端发,一端发,另一端收
  • 单工:一端只能收或发:例如管道

创建tcp链接

客户端

/**tcp客户端代码**/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

int fd; // 1. 创建监听文件描述符变量
void main()
{
    // 2. 创建链接 参数1:协议类型-ip4协议,数据载体-以网络数据流的方式,保护方式 0
    fd = socket(AF_INET, SOCK_STREAM, 0);
    // 3. 设置协议族(设置ip、端口)
    struct sockaddr_in sAddr = {0};
    sAddr.sin_family = AF_INET;                     // 协议类型 - ip4
    sAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置ip
    sAddr.sin_port = htons(9998);                   // 设置端口
    // 4. 链接服务端 参数:文件描述符,协议族取地址,字节数
    int r = connect(fd, (struct sockaddr *)&sAddr, sizeof(struct sockaddr_in));
    char data[256];
    while (1)
    {
        scanf("%s", data);
        // 5. 发送数据,参数:文件描述符,用来发送的变量,发送的最大字节数,返回发送成功的字节数
        r = send(fd, data, 255, 0);
        printf("send的数据%s\n", data);
        sleep(1);
    }
}
服务端
/**tcp服务端代码**/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

int fd, clientFd; // 1. 创建监听文件描述符变量
void main()
{
    // 2. 创建链接 参数1:协议类型-ip4协议,数据载体-以网络数据流的方式,保护方式 0
    fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd == -1)
    {
        printf("创建链接失败\n");
    }
    // 3. 设置地址族(设置ip、端口)
    struct sockaddr_in sAddr = {0};
    sAddr.sin_family = AF_INET;                     // 协议类型 - ip4
    sAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置ip
    sAddr.sin_port = htons(9998);                   // 设置端口
    // 4. 绑定地址族 参数:文件描述符,协议族取地址,字节数
    int r = bind(fd, (struct sockaddr *)&sAddr, sizeof(struct sockaddr_in));
    if (-1 == fd)
    {
        printf("绑定失败:%m\n");
        close(fd);
        exit(-1);
    }
    else
        printf("绑定 %m\n");

    // 5. 监听
    r = listen(fd, 10); // 参数:文件描述符,最大连接个数
    if (fd == -1)
    {
        printf("监听失败\n");
        close(fd);
    }
    else
        printf("监听成功\n");

    // 6. 接受客户端连接
    clientFd = accept(fd, NULL, NULL);
    if (-1 == clientFd)
    {

        printf("服务器崩溃:%m\n");
        close(fd);
        exit(-1);
    }
    printf("有客户端连接上服务器了:%d\n", clientFd); // 一般是4

    char data[256] = {0};
    while (1)
    {
        // 7. 接收数据
        r = recv(clientFd, data, 255, 0);
        if (r == -1)
        {
            printf("接收失败%d\n", r);
        }
        else if (r == 0)
        {
            printf("r:%d\n", r);
        }
        else
        {
            printf("recv:%s\n", data);
        }
        sleep(1);
    }
}

udp

创建服务端

/***
 * udp服务端
 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
int fd; // 1. 创建文件描述符变量
void main()
{
    // 2. 创建socket 参数:协议类型-ip4,数据载体-以网络数据流的方式,保护方式-0
    fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == fd)
        printf("创建socket失败:%m\n"), exit(-1);
    printf("创建socket %m\n");
    // 3. 设置服务器地址族
    struct sockaddr_in Addr = {0};
    Addr.sin_family = AF_INET;                     // 协议类型 - ip4
    Addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置ip
    Addr.sin_port = htons(9998);                   // 设置端口
    // 4. 绑定地址族 参数:文件描述符,协议族取地址,字节数
    int r = bind(fd, (struct sockaddr *)&Addr, sizeof(struct sockaddr_in));
    if (-1 == fd)
    {
        printf("绑定失败:%m\n");
        close(fd);
        exit(-1);
    }
    else
        printf("绑定 %m\n");

    char buff[256];
    int len = sizeof Addr;
    while (1)
    {
        // 6. 接收数据 参数 文件描述符,接收的数据,接收的最大数据长度,标志位(一般写0),接收目标地址,字节大小
        r = recvfrom(fd, buff, 255, 0, (struct sockaddr *)&Addr, &len);
        if (r > 0)
        {
            buff[r] = 0;
            printf("recvfrom:%s\n", buff);
        }
        sleep(1);
    }
}

创建客户端

/***
 * udp客户端
 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
void main()
{
    // 1. 创建socket并赋值给文件描述符变量fd 参数:协议类型-ip4,数据载体-以网络数据流的方式,保护方式-0
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == fd)
        printf("创建socket失败:%m\n"), exit(-1);
    printf("创建socket %m\n");
    // 2. 设置服务器地址族
    struct sockaddr_in sAddr = {0};
    sAddr.sin_family = AF_INET;                     // 协议类型 - ip4
    sAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置ip
    sAddr.sin_port = htons(9998);                   // 设置端口
    char buff[256];
    while (1)
    {
        scanf("%s", buff);
        // 3. 发送数据 参数:文件描述符,发送的数据,发送的数据长度,标志位(一般写0),发送目标地址,字节大小
        int r = sendto(fd, buff, strlen(buff), 0, (struct sockaddr *)&sAddr, sizeof sAddr);
        if (r > 0)
        {
            buff[r] = 0;
            printf("sendTo:%s\n", buff);
        }
    }
}

io多路复用

io多路复用简介

  1. 之前做网络服务器使用的是并发方式实现,并发方式实现会导致如果连接数多,创建并维护线程的开销会比较大。所以之前的大厂都用的多线程架构,后面转为io多路复用方式
  2. 服务器分为两半:一般处理连接的,采用io多路复用。处理io的还是多线程
  3. io多路复用本质为io异步操作,有点像线程同步。
  4. 是分时操作io描述符号(fd),造成并发的效果。
  5. 有三种方式:select、poll、epoll

select

文件描述符号集合:就是一个数组或者一个链表,里面有多个文件描述符号,像信号集。 原理:实时监视文件描述符号集合里的文件描述符号。如果挪个文件描述符号有变化,就返回,返回值 会不一样,能够体现是哪一个描述符号,我们一般通过实时循环检查select返回值来知道是那个描述符号有变化,然后就去对这个描述符号进行对应的io操作

poll

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <poll.h>

int main(){
    struct pollfd pfd = {0};

    pfd.fd = 0; // 将输入系统自带的文件描述符绑定到poll结构体
    pfd.events = POLLIN; // 将POLLIN(文件描述符上有 普通数据可读)绑定到poll输入事件

    while(1){
        int r = poll(&pfd,1,0); // 初始化poll,参数2为监听的文件描述符数量,参数3为延迟时间,返回值为失败-1,成功没动静是0,有动静是正数
        if(r<0) printf("出现了bug,快叫雍正来~~~~\n");
        else if(0 == r) continue;//printf("没有动静~~~~\n");
        else{
            printf("有动静:%d\n",r);
            // 判断当返回事件和我们设置的事件相同的时候,执行业务逻辑
            if(pfd.revents&POLLIN){
                char buff[256];
                r = read(0,buff,255);
                if(r > 0){
                    buff[r] = 0;
                    printf("》》%s\n",buff);
                }
            }
        }
        //sleep(1);
    }
    return 0;
}

epoll

轮询机制
  • 逐个检查描述符号
  • select和poll如果描述符号数量增多 ,那么效率会降低。
  • 描述符号数量在10000个以下,选择使用select或者是poll比较合适
事件机制,中断机制
  • 不用逐个检查描述符号,绑定描述符号变化和对应的处理,一旦有描述符号变化,直接执行对应处理(类似信号处理)
epoll原理

注册号对事件的处理,然后阻塞式等待发生某种事件,当事件发生后,针对描述符号来进行对应操作

异步io

异步io是系统调用执行后,会启动操作系统内核中的函数去做具体的io,然后应用程序可以做其他事情,轮询方式查询或者事件通知方式获取内核中函数已经完成io操作再去读取数据。

轮询异步写操作

#include <stdio.h>
#include <fcntl.h>
#include <aio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

typedef struct
{
    int id;
    char name[20];
    double score;
    int age;
} Student;

/***
 * a.用异步写方式写入自己的信息到文件中,同时实时显示当前系统时间
 */
int main()
{
    Student s = {7, "邓良斌", 11.11, 11};
    int fd = open("stu.ent", O_CREAT | O_WRONLY, 0666);
    if (-1 == fd)
        printf("打开文件失败:%m\n"), exit(-1);
    printf("打开文件成功!\n");
    // 定义一个aiocb结构体指针,并申请内存
    struct aiocb *pcb = malloc(sizeof(struct aiocb));
    // 清空申请的内存
    memset(pcb, 0, sizeof(struct aiocb));
    // 设置aiocb结构体指针的成员值
    pcb->aio_buf = &s; // 设置异步写缓冲区
    pcb->aio_fildes = fd; // 设置文件描述符
    pcb->aio_lio_opcode = LIO_WRITE; // 设置异步写操作类型,这里是写操作
    pcb->aio_nbytes = sizeof(s); // 设置异步写缓冲区的大小
    // 开始异步写,参数:pcb结构体指针
    int r = aio_write(pcb); // 返回值:成功返回0,失败返回-1
    printf("aio_write:%d\n", r);
    while (1)
    {
        // 显示当前系统时间
        time_t t = time(NULL);
        printf("当前系统时间:%s\n", ctime(&t));
        sleep(1);
        // 等待内核线程异步写完毕 返回值:成功返回0,失败返回-1
        int a = aio_error(pcb);
        if (a == 0){
            break;
        }
    }
    printf("操作完毕!\n");
    close(fd);
    free(pcb);
    return 0;
}

轮询异步读操作

#include <stdio.h>
#include <fcntl.h>
#include <aio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

typedef struct
{
    int id;
    char name[20];
    double score;
    int age;
} Student;

/***
 * a.b.用异步读方式读出来并打印到终端,同时实时显示当前系统时间
 */
int main()
{
    Student s = {};
    // 打开文件
    int fd = open("stu.ent", O_RDONLY);
    if (-1 == fd)
        printf("打开文件失败:%m\n"), exit(-1);
    printf("打开文件成功!\n");
    // 定义aiocb结构体,并申请内存
    struct aiocb *pcb = malloc(sizeof(struct aiocb));
    memset(pcb, 0, sizeof(struct aiocb)); // 清空申请的内存
    pcb->aio_buf = &s;// 设置异步读的缓冲区地址
    pcb->aio_fildes = fd;// 设置文件描述符
    pcb->aio_lio_opcode = LIO_READ; // 设置异步读操作,这里是读操作
    pcb->aio_nbytes = sizeof(s);// 设置异步读的长度
    // 开始异步读 返回值为0表示成功,其他值表示失败
    int r = aio_read(pcb);
    printf("aio_read:%d\n", r);

    while (1)
    {
        // 显示当前系统时间
        time_t t = time(NULL);
        printf("当前系统时间:%s\n", ctime(&t));
        sleep(1);
        // 等待内核线程异步写完毕
        int a = aio_error(pcb); // 返回值为0表示成功,其他值表示失败
        if (a == 0)
        {
            break;
        }
    }

    printf("操作完毕!\n");
    printf("%d:%s %g %d\n",
           s.id, s.name, s.score, s.age);
    close(fd);
    free(pcb);
    return 0;
}

异步中断

数据库

轻量级数据库sqlite

  • 很小,只有几M
  • 适合单片机内使用

常用数据库mySql

-- -- 启动mysql 登录数据库 admin/admin123
-- mysql -uroot -p

-- sudo service mysql status   查看状态
-- sudo service mysql  start     开启服务
-- sudo service mysql stop       关闭服务
-- sudo service mysql restart   重启服务

-- 配置外网可访问数据库
-- 1.修改MySQL绑定地址 编辑配置文件:
-- sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
-- 找到 bind-address 行,修改为:
-- bind-address = 0.0.0.0  # 允许所有IP访问
-- 2.重启MySQL
-- sudo service restart mysql
-- ​3. 创建远程访问用户
-- sudo mysql -u root -p
-- 执行以下SQL(替换username和password):
CREATE USER 'admin'@'%' IDENTIFIED BY 'admin123'; -- 创建用户,允许从任何机器链接数据库
GRANT ALL PRIVILEGES ON *.* TO 'username'@'%' WITH GRANT OPTION; -- 授权admin所有权限
FLUSH PRIVILEGES;-- 刷新权限表

/**
 * 创建名为 `chat_room_db聊天室` 的数据库,并设置默认字符集为 `utf8mb4`,默认排序规则为 `utf8mb4_general_ci`。
 * 该数据库用于存储用户相关信息。
 */
create database chat_room_db default charset utf8mb4 collate utf8mb4_general_ci;

-- 查看当前已有数据库
show databases;

-- 删除库
drop database chat_room_db;

-- 使用数据库名
use chat_room_db;

/*
 * 创建用户表 `user`,用于存储用户的基本信息。
 * 
 * 表结构说明:
 * - `id`: 用户唯一标识,主键,自增。
 * - `username`: 用户名,唯一且不能为空,最大长度为20个字符。
 * - `email`: 用户邮箱,唯一且不能为空,最大长度为40个字符。
 * - `phone`: 用户手机号,最大长度为11个字符。
 * - `nicheng`: 用户昵称,最大长度为20个字符。
 * - `password`: 用户密码,不能为空,最大长度为20个字符。
 * - `age`: 用户年龄,可为空。
 * - `sex`: 用户性别,枚举类型,可选值为 '男' 或 '女'。
 * - `level`: 用户等级,可为空。
 * - `area`: 用户所在地区,最大长度为20个字符,可为空。
 * - `signature`: 用户个性签名,最大长度为100个字符,可为空。
 */
 create table user(
    id int primary key auto_increment,
    username varchar(20) not null unique,
    email varchar(40) not null unique,
    phone varchar(11) not null,
    nicheng varchar(20) not null,
    password varchar(20) not null,
    age int,
    sex enum('男', '女'),
    level int,
    area varchar(20),
    signature varchar(100)
);

-- 查看当前库中的表
show tables;

-- 查看表结构
describe user;

-- 删除表
drop table user;

-- 添加
insert into user(username, email, phone, nicheng, password, age, sex, level, area, signature) 
values('admin', 'admin@qq.com', '12345678901', 'admin', 'admin123', 18, '男', 1, '北京', '我是一个管理员');
insert into user(username, email, phone, nicheng, password, age, sex, level, area, signature) 
values('admin2', 'admin2@qq.com', '12345678901', 'admin2', 'admin123', 18, '男', 1, '北京', '我是一个管理员');


-- 修改密码
update user set password = '123' where id = 1;


/*
 * 创建聊天室表 `chat_room`,用于存储聊天室相关信息。
 * 
 * 表结构说明:
 * - `id`: 聊天室唯一标识,主键,自增。
 * - `room_name`: 聊天室名称,唯一且不能为空,最大长度为40个字符。
 * - `password`: 聊天室密码,可为空,最大长度为20个字符。
 * - `max_user`:聊天室最大用户数,默认为10。
 * - `create_time`: 聊天室创建时间,默认为当前时间。
 */
 create table chat_room(
    id int primary key auto_increment,
    room_name varchar(40) not null unique,
    password varchar(20),
    max_user int default 10,
    create_time datetime default current_timestamp
);
-- 添加
insert into chat_room(room_name, password, max_user) values('聊天室1', '123', 10);
-- 删除
delete from chat_room where id = 1;

/*
 * 创建聊天室成员表 `chat_room_member`,用于存储聊天室成员相关信息。
 * 
 * 表结构说明:
 * - `id`: 聊天室成员唯一标识,主键,自增。
 * - `room_id`: 聊天室ID,外键,引用 `chat_room` 表的 `id` 字段。
 * - `user_id`: 用户ID,外键,引用 `user` 表的 `id` 字段。
 * - `join_time`: 成员加入时间,默认为当前时间。
 * - `is_admin`: 是否为管理员,默认为0,表示普通成员。
 */
 create table chat_room_member(
    id int primary key auto_increment,
    room_id int,
    user_id int,
    join_time datetime default current_timestamp,
    is_admin tinyint default 0,
    foreign key (room_id) references chat_room(id),
    foreign key (user_id) references user(id)
);
-- 增加聊天室成员
insert into chat_room_member(room_id, user_id) values(1, 1);
-- 删除聊天室成员
delete from chat_room_member where room_id = 1 and user_id = 1;
-- 修改聊天室成员为管理员
update chat_room_member set is_admin = 1 where room_id = 1 and user_id = 1;

/*
 * 创建聊天室消息表 `chat_room_message`,用于存储聊天室消息相关信息。
 * 
 * 表结构说明:
 * - `id`: 消息唯一标识,主键,自增。
 * - `room_id`: 聊天室ID,外键,引用 `chat_room` 表的 `id` 字段。
 * - `user_id`: 用户ID,外键,引用 `user` 表的 `id` 字段。
 * - `content`: 消息内容,最大长度为200个字符。
 * - `send_time`: 消息发送时间,默认为当前时间。
 */
 create table chat_room_message(
    id int primary key auto_increment,
    room_id int,
    user_id int,
    content varchar(200),
    send_time datetime default current_timestamp,
    foreign key (room_id) references chat_room(id),
    foreign key (user_id) references user(id)
);
-- 新增消息
insert into chat_room_message(room_id, user_id, content) values(1, 1, '你好');
-- 删除消息
delete from chat_room_message where id = 1;





拓展通信协议

http

ssl

https

smtp

pppoe