Linux编程

Linux编程

文件操作

  • 通过<errno.h>和<string.h>头文件可以获得文件错误参数
    • errno代表错误id,是包含在errno.h文件中的全局变量
    • 通过strerror(int errno)函数可以将errno的id转为人们可以理解的字符串
    • 可以直接通过perror(char str*)遇到错误输出对应的str字符串和错误原因

#include <stdio.h>
#include <string.h>
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc,char **argv){
    int fd = open(argv[1], O_RDONLY);
    if (fd == -1)
    {
        //printf("error = %d, error:%s\n", errno, strerror(errno));
        perror("file error : ");
        return -1;
    }
    printf("open ok\n");
    return 0;
}


  • 创建文件
int create(const char *pathname, mode_t mode);

这里的 *pathname就是创建文件的路径名称
mode就是文件的操作权限(三类用户的操作权限:所属用户,所属用户组,其他用户;操作分为:读,写,操作;操作就是linux系统中的权限777一类)
如果创建失败则返回-1并将全局的errno错误变量设置值,创建成功则返回新的文件描述符


#include <stdio.h>
#include <string.h>
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc,char **argv){
    // int fd = creat(argv[1], S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
    int fd = creat(argv[1], 0777); // 开头一个0表示二进制,之后一次是用户,用户组,其他用户的权限
    // r 读 4 w 写 2 e 执行 1
    if (-1 == fd)
    {
        perror("文件creat");
    }
    printf("create %s ok \n ", argv[1]);
    return 0;
}


  • 打开文件

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

*pathname打开文件的路径以及文件名;mode_t一般在创建文件时使用(即使用第二个open函数)
flags说明打开文件的原因:O_RDONLY(只读),O_WRONLY(只写),O_RDWR(读写)【此三个必写】;O_CREAT(文件不存在则创建),O_APPEND(自动写到文件末尾),O_TRUNC打开文件时清空文件……
如果open成功则返回一个非负整数作为文件描述符,如果创建失败则返回-1


#include <stdio.h>
#include <string.h>
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc,char **argv){
    int fd = open(argv[1], O_RDWR);
    if (-1 == fd)
    {
        perror("open file"); // 如果文件不存在不会自动创建文件,需要flags中添加 O_CREAT才会创建
        return 1;
    }
    printf("open %s ok", argv[1]);  

    int fd2 = open(argv[2], O_RDWR | O_CREAT, 0777); // 0表示八进制, 777 表示所有人都可以读写执行

    return 0;
}


  • 写文件

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

fd就是文件描述符,*buf写入数据的地址,count写入数据的大小
buf指向空间的可能有20kB,你只需要写10KB,这时候count的作用就来了
返回值ssize_t 是一个整数,表示实际写入的字符;可能写入的时候出现了中断等操作,导致剩余的无法写入
有一个RLIMIT_FSIZE宏,定义了一次可写入的最大字节数


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc,char **argv){
    // 如果文件open成功,默认偏移位置在文件首部,这时候写会覆盖旧的数据
    // 偏移量随着读写操作往后偏移
    int fd = open(argv[1], O_RDWR | O_CREAT , 0777);
    if (-1 == fd)
    {
        perror("open file");
        return -1;
    }
    printf("open %s ok\n", argv[1]);  
    ssize_t ret = -1; // ssize_t 非负整数
    char strBuf[128] = {'\0'};
    while (1)
    {
        printf("请输入数据:\n");
        memset(strBuf, 0, sizeof(strBuf));
        scanf("%s", strBuf);
        if (strcmp(strBuf, "exit") == 0)
        {
            close(fd); // 关闭文件
            return 0;
        }
        ret = write(fd, strBuf, strlen(strBuf));
        if (-1 == ret)
        {
            perror("write file");
            return -1;
        }
    }
    return 0;
}

  • 读文件

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

fd是文件描述符,buf是写入的指针地址
count是设置读入的大小
如果读写成功返回读到的字节数,发生错误返回-1


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc,char **argv){
    int fd = open(argv[1], O_RDWR);
    if (-1 == fd)
    {
        perror("open file");
        return -1;
    }
    printf("open %s ok\n", argv[1]);  
    ssize_t ret = -1; // ssize_t 非负整数
    char strBuf[128] = {'\0'};
    while (1)
    {
        memset(strBuf, 0, sizeof(strBuf));
        ret = read(fd, strBuf, sizeof(strBuf));// 把strBuf读满
        if (-1 == ret)
        {
            perror("read file");
            return -1;
        } else if (0 == ret)
        {
            break; // 读到文件末尾
        }
        printf("读到的数据 %s \n", strBuf);
    }

    return 0;
}


  • 目录操作
    • 创建目录
    • 打开目录
    • 读目录
    • 关闭目录
    • 扫描目录

#include <sys/stat.h>
#include <sys/types.h>

int mkdir(const char *pathname, mode_t mode);

#include <fcntl.h>           /* Definition of AT_* constants */
#include <sys/stat.h>

int mkdirat(int dirfd, const char *pathname, mode_t mode);

*pathname 创建文件夹的路径,mode 文件夹的访问权限
返回值为 如果创建成功返回0,如果失败返回-1,同时会设置errno.h头文件中的errno的全局变量

  • access()函数:在创建文件夹之前先检查文件是否存在,如果文件存在或没有权限创建则不能创建

#include <unistd.h>

int access(const char *pathname, int mode);

#include <fcntl.h>           /* Definition of AT_* constants */
#include <unistd.h>

int faccessat(int dirfd, const char *pathname, int mode, int flags);

mode为检测操作权限:R_OK(读),W_OK(写),X_OK(执行),F_OK(文件是否存在)
如果mode为F_OK并且文件存在,返回值为0,;文件不存在返回-1


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

int main(int argc, char **argv){

    // 首先检查要创建的目录是否存在
    int ret = -1;
    int i=1;
    for (; i < argc; i++){
        ret = access(argv[i], F_OK);
        if (ret == 0)// 文件存在
        {
            printf("%s file exist", argv[i]);
            continue;
        }
        ret = mkdir(argv[i], 0777);
        if (ret == -1)
        {
            perror("dir create error");
            continue;
        }
        printf("%s create success\n", argv[i]);
    }
    printf("all created\n");
    return 0;
}


  • 重新设置文件读写位置
    • 读写位置随着读写的顺序往后移动
    • 使用lseek()函数可以重新定位读写的偏移量

#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);

whence包括:SEEK_SET(文件首部),SEEK_CUR(当前文件读写位置),SEEK_END(文件末尾) 相当于一个参照点
offset:以whence为基点的偏移量(可以正数也可以负数)
返回值为读写位置距离文件首部的字节数,如果设置偏移失败则返回-1同时设置errno的值


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc,char **argv){

    int fd = open(argv[1], O_RDWR);
    off_t offset = -1;
    offset = lseek(fd, 0, SEEK_END); // 定位到文件末尾
    if (offset == -1)
    {
        perror("file lseek");
        return -1;
    }
    printf("file size %ld B", offset);//返回值为偏移位置距离首部的字节
    // 如果定位到末尾,返回值便是末尾到首部的字节数也就是整个文件的字节数
    // 故这里的printf的值就是文件的大小

    return 0;
}


  • 创建目录

#include <sys/types.h>
#include <dirent.h>

DIR *opendir(const char *name);
DIR *fdopendir(int fd);

name 你要打开的文件夹路径
返回的指针可用于后续对文件夹的操作
如果函数执行成功返回DIR*,如果执行失败返回NULL同时设置errno的值

  • 打开目录

#include <dirent.h>
struct dirent *readdir(DIR *dirp);

struct dirent {
    ino_t          d_ino;       /* Inode number 节点编号 */
    off_t          d_off;       /* Not an offset; see below 偏移 */
    unsigned short d_reclen;    /* Length of this record  */
    unsigned char  d_type;      /* Type of file; not supported
                                    by all filesystem types 文件类型 */
    char           d_name[256]; /* Null-terminated filename 文件名 */
};

// 以下为d_type的宏定义
DT_BLK      This is a block device.
DT_CHR      This is a character device.
DT_DIR      This is a directory.
DT_FIFO     This is a named pipe (FIFO).
DT_LNK      This is a symbolic link.
DT_REG      This is a regular file.
DT_SOCK     This is a UNIX domain socket.
DT_UNKNOWN  The file type could not be determined

readdir()能够读取opendir()的返回值并将文件夹中某一个文件的信息返回为结构体
通过结构体可以获得文件夹的信息
如果打开发生错误返回NULL同时errno的值会设置;如果读到末尾还再往后读返回NULL但是errno的值不会被修改


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>

int main(int argc,char **argv){
    DIR* pDir = opendir(argv[1]);
    if (pDir == NULL)
    {
        perror("dir open");
        return -1;
    }
    printf("open dir success");

    struct dirent *pDirent = NULL;
    while (1)// 循环遍历文件夹内所有的文件
    {
        pDirent = readdir(pDir);
        if (pDirent == NULL)
        {
            break;
        }
        printf("%s ", pDirent->d_name);
        switch (pDirent->d_type)
        {
        case DT_BLK:
            printf("type : This is a block device.\n");
            break;
        case DT_CHR:
            printf("type : This is a character device.\n");
            break;
        case DT_DIR:
            printf("type : This is a directory.\n");
            break;
        case DT_FIFO:
            printf("type : This is a named pipe (FIFO).\n");
            break;
        case DT_LNK:
            printf("type : This is a symbolic link.\n");
            break;
        case DT_REG:
            printf("type : This is a regular file.\n");
            break;
        case DT_SOCK:
            printf("type : This is a UNIX domain socket.\n");
            break;
        default:
            printf("type is unknow\n");
            break;
        }
    }
    closedir(pDir);
    return 0;
}

这里可以使用递归和循环查找目标文件夹中的所有文件

进程

  • 程序

    • 编译好的二进制文件
  • 进程

    • 运行着的程序
  • 站在程序的角度:运行一系列指令的过程

  • 站在操作系统的角度:分配系统资源的基本单位

  • 区别:

    • 程序占用磁盘,不占用系统资源(CPU,内存)
    • 内存占用系统资源
    • 一个程序对应多个进程,一个进程只能对应一个程序
    • 程序没有生命周期,进程有生命周期
  • 程序的状态

    1. 就绪态:等待系统分配运行时间,才能转为运行态
    2. 运行态:得到系统资源,并且正在运行;系统分配的运行时间完毕之后转为就绪态
    3. 阻塞态:等待资源满足运行需求,才能转为就绪态
  • PCB并发:进程控制块,其结构包括下面几个

    1. 程序ID(PID、进程句柄):它是唯一的,一个进程都必须对应一个PID。PID一般是整形数字
    2. 特征信息:一般分系统进程、用户进程、或者内核进程等
    3. 进程状态:运行、就绪、阻塞,表示进程现的运行情况
    4. 优先级:表示获得CPU控制权的优先级大小
    5. 通信信息:进程之间的通信关系的反映,由于操作系统会提供通信信道
    6. 现场保护区:保护阻塞的进程用
    7. 资源需求、分配控制信息
    8. 进程实体信息,指明程序路径和名称,进程数据在物理内存还是在交换分区(分页)中
    9. 其他信息:工作单位,工作区,文件信息等
  • 常见的进程调度算法

    1. 先来先服务(FIFO)
    2. 短作业(进程)优先调度算法
    3. 高优先权优先调度算法
      • 抢占式
      • 非抢占式
    4. 高响应比优先调度算法
    5. 时间片轮转调度算法
    6. 多级反馈队列调度算法

操作系统将多种调度算法混合在一起,协调管理进程
宏观上多个进程同时运行,微观上只有一个进程在运行(只有CPU的情况下)

  • 环境变量

    1. PATH:可执行文件的搜索路径
    2. SHELL:当前Shell,通常是/bin/bash
    3. TERM:当前终端类型,图形界面下通常是xterm
    4. LANG:语言和local
    5. HOME:家目录
  • 使用getenv函数获取环境变量的函数


#include <stdlib.h>
#include <stdio.h>

int main(){
    printf("%s", getenv("HOME"));
    return 0;
}

  • 读取.bashrc文件获取环境变量

  • fork()函数:创建一个新的进程
pid_t fork(void)

返回值为创建的子进程的pid
如果创建失败则返回-1

  • getpid()获取进程pid
  • getppid()获取父进程pid

#include <unistd.h>
#include <stdio.h>
int main(){
    printf("begin...\n");
    pid_t pid = fork();
        if(pid < 0){
        printf("error pid\n");
        exit(1);
    }
    if(pid == 0){
        printf("son pid = %d , ppid = %d\n",getpid() ,getppid());
    }
    else if(pid > 0){
        printf("father pid = %d, ppid = %d my son's pid = %d\n", getpid(), getppid(), pid);
    }
    printf("end...\n");
    return 0;
}

fork()创建子进程的过程:
先对父进程进行一次拷贝(代码,变量,运行状态等都拷贝一遍,将父进程完全复制一次)
父进程fork()返回子进程pid,子进程fork()返回0,当子进程的fork()返回0之后子进程才算真正开始执行


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>

int main(){
    int i=0;
    for (; i<2; i++){
        fork();
        printf("hello world\n");
        // printf("hello world ");
    }
    return 0;
}

如果是非注释的printf会输出6次hello world
如果是注释的printf会出书8次hello world

小知识


#include <stdio.h>

int main(){
    printf("hello world");
    while (1)
    {
        /* code */
    }
    return 0;
}

  • 上述代码执行的时候并不会输出 “hello world”
  • 但是将hello world换成 printf(“hello world\n”);会正常输出
  • 每个进程都有自己的数据缓冲区
    1. 当其中的数据含有’\n’,则不管缓冲区中的数据有多少,都进行刷新
    2. 其中没有’\n’,缓冲区中的数据需要等到满了或者自动刷新的时间到了才能进行刷新
    3. 若程序正常结束,缓冲区中的数据会被刷新

上述的输出6次和8次hello world的代码中区别就是缓冲区的不同
printf(“hello world\n”)中缓冲区每次立即输出并立即刷新
printf(“hello world”)中缓冲区等到 满/换行符/程序结束 刷新
第二层fork()将父进程缓冲区中的"hello world"也复制过来了


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>

int main(){
    fork() && fork() || fork();
    printf("f\n");
    return 0;
}

问:输出几次f,为什么?


  • 僵尸进程:进程结束之后到被回收的期间,等待回收(进程号,进程资源等)
    • 僵尸进程通常由其父进程处理
    • 如果父进程存在,且没有对其处理,则僵尸进程会一直存在
  • 危害:
    • 若系统中存在大量的僵尸进程,占用的资源不能被回收,可能导致系统无法创建新的进程
    • kill -9命令无法杀死僵尸进程,因为僵尸进程已经死亡,只是占着资源
  • 处理僵尸进程:
    • 将僵尸进程改为孤儿进程(父进程死亡并释放),那孤儿进程由祖宗进程释放
    • 父进程使用回收僵尸进程的函数:wait()

解决方法一:


#include <sys/types.h>
#include <sys/wait.h>
// 等待进程改变其状态 生->死
pid_t wait(int *wstatus);// 可以回收僵尸进程

pid_t waitpid(pid_t pid, int *wstatus, int options);

int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

wstatus 接收进程的返回状态(返回值,是否正常结束等),如果不想获得返回状态直接写NULL即可
这里status是引用,wait函数中会设置status的值,将进程子信息结束信息传递出来
通过WIFEXITED(status)宏函数判断子进程是否正常结束,如果该宏函数返回true则正常结束
通过WEXITSTATUS(status)宏函数来获得子进程的返回值
wait()的返回值为其回收进程的进程号
如果成功返回结束进程的进程号,如果失败返回-1


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/wait.h>

int main(){
    pid_t pid = -1;
    pid = fork();
    if (pid > 0) // parent
    {
        pid_t tmp = -1;
        int status = 0;
        tmp = wait(&status);
        // 如果子进程结束,则父进程一直处于阻塞状态等待子进程结束
        // 如果有某一个子进程结束,则父进程马上对其回收
        // 回收之后不再等待,进行往后运行
        printf("wait pid = %d and child pid = %d and child status = %d \n", tmp, pid, WEXITSTATUS(status));
    }
    else if (pid == 0)
    {
        int num = 4;
        while(num--){
            sleep(1);
            printf("childe hello world\n");
        }
        return 256 ; // 返回值不要高于255,否则会出事
    }else {
        perror("fork ");
        return -1;
    }
    return 0;
}

这里代码有一点欠缺:如果子进程一直不结束,那么父进程会一直等,这跟单进程没什么区别吧

解决方法二:

  • 为解决上面的问题:主进程A创建进程B并wait进程B,进程B只创建进程C就结束,进程B原本的工作交给进程C,那么进程B的资源就会通过wait函数回收,同时进程C失去父进程成为孤儿进程由祖进程接管

#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/wait.h>

int main(){
    pid_t pid = -1;
    pid = fork(); // A进程创建B进程
    if (pid > 0)
    {
        pid_t tmp = -1;
        wait(NULL);
        printf("father process finish\n");
    }
    else if (pid == 0)
    {
        int pid2 = fork();
        if (pid2 == -1)
        {
            perror("fork 2 ");
            return -1;
        } else if(pid2 == 0){ // C进程做事
            int num = 4;
            while(num--){
                sleep(1);
                printf("do what child do\n");
            }
        } else { // B进程负责创建
            printf("Process C create Finish\n");
            return 66;
        }
    }else {
        perror("fork ");
        return -1;
    }
    return 0;
}

解决方法三:

  • 通过对子进程结束时自动发出的信号处理方式重定义来处理僵尸进程

  • 其他创建进程的方法:exec函数族(一系列exec开头的函数)

#include <unistd.h>

extern char **environ;

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);

  • exec()函数族通过执行文件创建进程,exec()函数组创建的新进程会替换当前进程,但是会沿用原来进程的进程号(pid)

  • execl(const char *path, const char *arg, …)

    • path 想要执行的文件的路径,arg…文件执行的语法方式
    • 只有当错误发生的时候才会有返回值(进程创建错误)

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

int main(){
    printf("begin main\n");
    int num = 4;
    while (num --)
    {
        printf("this procee pid = %d\n", getpid());
        sleep(1);
    }
    int ret = execl("/usr/bin/gedit", "gedit", "test.txt", NULL);
    // path = "/user/bin/gedit" 到给定的路径下面去找要执行的命令
    // arg... = "getdit","test.txt" 若命令找到了,则将后续的执行方式组装起来,形成可执行的格式,拼接的时候遇到NULL停止拼接
    // 上述函数等同命令行 = getdit test.txt
    if (-1 == ret)
    {
        perror("execl ");
    }
    printf("end main\n");
    return 0;
}

上述的getdit程序可以换成自己的程序,例如 “g++ test.cpp -o test”,那么可以换成execl("/home/user/test", “test”, NULL);

  • execlp(const char *file, const char *arg, …)
    • file 要执行的文件名而非路径,其他的差不多

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

int main(){
    printf("begin main\n");
    int num = 4;
    while (num --)
    {
        printf("this procee pid = %d\n", getpid());
        sleep(1);
    }
    // 到系统默认的路径(配置在环境变量中 bash的PATH中)下寻找要执行的文件
    // 若找到则按照arg指定的指定的执行方式进行运行

    int ret = execlp("gedit", "gedit", "test.txt", NULL);
    if (-1 == ret)
    {
        perror("execlp ");
    }
    printf("end main\n");
    return 0;
}

与上述的execl函数差别不大,但是无法执行自己的程序,因为自己的程序并未加入到bash的PATH变量中
想要用execlp执行自己的程序只能通过把自己程序的文件路径添加到PATH中才行
要想获取环境变量,上述的enev函数可以获得;或编辑.bash文件;或编辑/etc/profile文件

  • execv(const char *path, char *const argv[])
    • 与上述函数相似,不同的地方是将执行格式放到一个数组中

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

int main(){
    printf("begin main\n");
    int num = 4;
    while (num --)
    {
        printf("this procee pid = %d\n", getpid());
        sleep(1);
    }
    char *argv[] = {"gedit", "test.txt", NULL};
    int ret = execv("/usr/bin/gedit", argv);
    if (-1 == ret)
    {
        perror("execv ");
    }
    printf("end main\n");
    return 0;
}


  • 仿终端设计思路:
    • 直接使用exec族函数会替换主函数
    • 主函数接收参数,主函数fork()一个子进程B
    • 在子进程B中执行exec函数族,这样不影响主进程

进程间通信

  • 相互独立的进程都是通过第三方进行通信的

    1. 本地进程间通信
      • 内核空间中申请一块空间,A进程写入数据,B进程读入数据;反之亦然
      • A进程在磁盘中写一个文件,B进入读入文件
    2. 异地进程间通信
      • 借助网络通信,传递信息
  • 通讯方式

    1. 无名管道:亲缘进程间通信
    2. 有名管道:任何进程可通信
    3. 共享内存
    4. 文件内存映射
    5. 消息队列
    6. 信号
    7. 信号量
    8. socket

1~7属于本地进程通信
8 属于网络进程通信


  • 无名管道
    • 在内核空间内申请一块独立进程空间
    • 空间提供两个接口:一个接口用于写数据,一个接口用于读取数据
    • 因为是无名管道,所以其他进程无法访问,创建的进程才能获得管道接口
      • 父进程创建无名管道,获得管道读写接口
      • 子进程对父进程会进行一次拷贝,顾子进程也会保留一个无名管道的读写接口
    • 只能从写端写数据,读端读数据,数据读取完毕之后会自动清空(一端只能写,一端只能读)

#include <unistd.h>

int pipe(int pipefd[2]);

#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>

int pipe2(int pipefd[2], int flags);

pipefd[2]有两个元素用于存放管道的读写接口:文件描述符;写是pipefd[1],读是pipefd[0]
因为返回的是文件描述符(与open函数返回相同),所以可以使用write和read进行读写
如果创建管道成功返回0,失败返回-1并且设置errno错误编号


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>

int main(){
    int pipefd[2] = {0};
    int ret = -1;
    ret = pipe(pipefd);
    if (ret == -1)
    {
        perror("pipe create ");
        return -1;
    }
    pid_t pid = fork();
    if (pid > 0)
    {// parent
        char cMsg[64] = {'\0'};
        while (1)
        {
            memset(cMsg, 0, sizeof(cMsg));
            printf("parent, please input Msg : \n");
            scanf("%s", cMsg);
            write(pipefd[1], cMsg, strlen(cMsg));
        }
    } else if (pid == 0)
    {// child
        char cMsg[64] = {'\0'};
        while (1)
        {
            memset(cMsg, 0, sizeof(cMsg));
            read(pipefd[0], cMsg, sizeof(cMsg));
            printf("child output : %s\n", cMsg);
        }
    } else
    {
        perror("fork ");
        return -1;
    }
    return 0;
}

  • 有名管道:因为管道有名字,所以不同进程都可以访问

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

pathname 在哪里创建管道文件
mode 用户对管道文件的操作权限
如果成功返回值为0,失败返回-1同时设置errno的值


// 读取fifo的C文件
#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc, char* argv[]){

     int fd = open(argv[1], O_RDONLY);
     char cMsg[64] = {'\0'};
     while (1)
     {
         memset(cMsg, '\0', sizeof(cMsg));
         read(fd, cMsg, sizeof(cMsg));
         if (0 == strcmp("exit", cMsg))
         {
             break;
         }
         printf("get msg : %s\n", cMsg);
     }

    return 0;
}

// 写入fifo的C文件
#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc, char* argv[]){int ret = mkfifo(argv[1], 0777);
    if (-1 == ret)
    {
        perror("mkfifo ");
        return -1;
    }
    // 通过 ls -l 可查看创建的mkfifo文件的权限
    int fd = open(argv[1], O_WRONLY); // 这个c文件是专门写
    if (fd == -1)
    {
        perror("open ");
        return -1;
    }printf("mkfifo success at %s\n", argv[1]);
    char cMsg [64] = {'\0'};while (1)
    {
        printf("please input msg : \n");
        memset(cMsg, '\0', sizeof(cMsg));
        scanf("%s", cMsg);
        write(fd, cMsg, strlen(cMsg));
        if (0 == strcmp("exit", cMsg))
        {
            break;
        }
    }
    return 0;
}

上述两个C文件要同时执行,先执行下面的写文件在执行上面的读文件
有名管道中写打开管道时进程会进入阻塞状态,等到有另一个进程读打开同样的有名管道时才会解开阻塞

  • 共享内存:过个进程可以访问到

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

申请一块共享内存块,可以被多个进程访问到
key_t 是一个整数,作为空间的标识符/别名
size_t 是空间的大小
shmflg:是flag参数,IPC_CREAT:创建空间,制定访问权限
返回值会int型整数,如果创建成功返回共享内存的表示,类似open函数返回的文件描述符;如果错误返回-1并设置errno


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>


int main(int argc, char* argv[]){
    int shareId = shmget(0x1024, 4096, IPC_CREAT | 077);
    // 第一个作为空间标识,建议一16进制作为标识
    // 第二个参数表示创建4096个字节
    // 第三个IPC_CREAT表示创建空间 设置权限为 777
    // 若共享内存不存在则创建,若共享内存存在则直接返回共享内存的标识
    if (-1 == shareId)
    {
        perror("crate share memory : ");
        return -1;
    }
    printf("shareId = %d\n", shareId);
    // 使用 'ipcs -m' 查看内存分块,其中key_t形参对应的就是key的值,函数返回值shareId对应的就是shmid的值,nattch表示使用这块内存的进程数
    return 0;
}

  • 关联共享内存地址

#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);

int shmdt(const void *shmaddr);

关联前面创建的共享内存,返回值就是共内存的地址,得到地址就能进行读写操作
shmid 就是共享内存的id/标识;shmaddr是内存空间的地址,用于记录并关联共享内存到本进程内存内部,一般NULL就行了,让系统关联
shmflg是关联内存空间的作用:SHM_RDONLY(只读)和 0(读写) 两种情况,没有只读
如果成功返回关联共享内存的地址,如果错误返回-1并且设置errno


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main(int argc, char* argv[]){
    int shareId = shmget(0x1024, 4096, IPC_CREAT | 0777);
    // 第一个作为空间标识,建议一16进制作为标识
    // 第二个参数表示创建4096个字节
    // 第三个IPC_CREAT表示创建空间 设置权限为 777
    // 若共享内存不存在则创建,若共享内存存在则直接返回共享内存的标识
    if (-1 == shareId)
    {
        perror("crate share memory : ");
        return -1;
    }
    printf("shareId = %d\n", shareId);
    // 使用 'ipcs -m' 查看内存分块,其中key_t形参对应的就是key的值,函数返回值shareId对应的就是shmid的值,nattch表示使用这块内存的进程数

    void *addr = shmat(shareId, NULL, 0); // 0 表示读写 , SHM_RDONLY 表示只读
    if ((void*)-1 == addr)
    {
        perror("shmat ");
        return -1;
    }
    // 写入数据
    char *pData = "hello world";
    sprintf((char *)addr, "%s", pData); // 格式化的输入到空间中
    // strcpy((char *)addr, pData); 使用 strcpy 拷贝到共享内存中
    // memcpy(addr, data, strlen(data)); 使用 memcpy 直接拷贝整个内存拷贝

    return 0;
}

#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main(int argc, char* argv[]){
    int shid = shmget(0x1024, 4096, IPC_CREAT | 0777);
    void *addr = shmat(shid, NULL, 0); // 0 表示读写 , SHM_RDONLY 表示只读
    if ((void*)-1 == addr)
    {
        perror("shmat ");
        return -1;
    }
    char data[64] = {'\0'};
    memset(data, '\0', sizeof(data));
    // 读出数据
    sscanf((char*)addr, "%s", data); // 使用格式化输入 将数据从内存中读取数据
    // strcpy(data, (char *)addr); 使用 strcpy 从 共享内存中拷贝出来
    // memcpy(data, addr, strlen(data)); 使用 memcpy 直接拷贝整个内存出来
    // 空间使用完之后要解除空间关联
    shmdt(addr);

    printf("%s\n", data);
    return 0;
}

  • 内存映射:将文件的储存部分直接映射到内存里面去,这样直接操作内存空间即可
    • 如果内存空间的数据发生更改,物理文件也会跟着修改
    • 直接操作内存比操作文件快

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags,
        int fd, off_t offset); // 文件映射
int munmap(void *addr, size_t length); // 解除映射

addr是文件要映射到内存的内存地址,由于不知道哪块内存合适,可以直接写NULL
length映射内存空间的大小,fd映射文件的文件描述符, offset映射的起始位置(参考lseek函数)
prot是存储空间的作用:PROT_EXEC(内存空间是用来执行的),PROT_READ(空间是用来读的),PROT_WRITE(空间是用来写的),PROT_NONE(无作用),如果想要读写就用 ‘|’ 拼接
flag:MAP_SHARED(内存数据修改会写回文件,其他内存可以获得修改后的数据以此进行通信),MAP_PRIVATE(仅供本进程使用,不会更新到文件)
如果成功返回值为(void *)的指针,指向映射的内存;失败返回 MAP_FAILED 的宏,并且设置errno的值


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/mman.h>

int main(int argc, char* argv[]){
    void * addr = NULL;
    int fd = open(argv[1], O_RDWR);
    // 文件打开方式要与内存使用方式保持一致
    addr = mmap(NULL, 32, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    // NULL 系统自动分配合适内存地址, 32 开辟32字节大小空间, PROT_READ|PROT_WRITE 能读能写, fd 文件描述符, 0字节偏移
    if (addr == MAP_FAILED)
    {
        perror("mmap ");
        return -1;
    }
    char msg[40] = {'\0'};
    // 从内存拷贝数据
    memcpy(msg, addr, sizeof(msg));
    printf("msg = %s", msg);

    // 数据写入内存
    char *wMsg = "read_all";
    strcpy((char *)addr, wMsg);

    // 解除内存映射
    munmap(addr, 32);

    close(fd);
    return 0;
}

观察到的结果是文件32B的内容被输出,同时 “read_all” 写入覆盖并文件的前面几个字节
这功能可用于实现进程间通信,因为数据修改会被同步文件中
将硬件设备映射到内存中,简化对硬件设备的操作;例:摄像头的数据直接映射到内存中,那么捕获的数据也被映射到内存中,那么只用读取内存不用设备

信号

  • 操作系统中,信号用数字来表示; linux命令:kill -l等
  • PCB(进程控制块)中有一个数组表示是否收到某个信号
  • 若收到某个信号则将数组中相应位置的值设置为1,表示收到这个信号,操作系统会循环检查进程PCB中的数组是否有位置的值设置为1,若有则使用事先定义好的方式来处理该进程
  • 总共有64个信号,分别使用1~64来表示

 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	


#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

第一个参数是要发送进程的pid,第二个参数是发送信号量的值
如果信号发送成功返回0,如果失败返回-1


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <unistd.h>


int main(int argc, char* argv[]){
    printf("%d\n", getpid());
    while (1)
    {
    }
    return 0;
}

#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <stdlib.h>

int main(int argc, char* argv[]){
    printf("kill process id %s", argv[1]);
    pid_t pid = atoi(argv[1]);
    kill(pid, 9);
    // kill(pid, SIGKILL)
    printf("end \n");
    return 0;
}

先运行上面的程序获得死循环的pid,再运行下面的(9表示杀死进程)来杀死死循环的进程

  • 常见的信号
    • Ctrl + C : SIGINT (2号信号) 用于结束程序
    • Ctrl + Z : SIGTSTP (20号信号)
    • CTRL + \ : SIGQUIT (3号信号)
    • 子进程结束:自动发送信号——SGCHLD给父进程
    • 有名管道 : 通信失败会发送 SIGPIPE 信号
    • 闹钟信号 : alarm()函数产生的信号
    • 用户自定义信号:SIGUSR1,SIGUSR2
    • kill,raise和alarm函数可以发送信号

#include <signal.h>
int raise(int sig); // 发送信号给自己本身,所以不像kill有作用对象;可以自己杀自己…


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char* argv[]){
    printf("begin\n");
    int n = 4;
    while (n--)
    {
        sleep(1);
    }
    raise(SIGKILL);// 杀死自己
    // 进程收到信号,首先处理信号,若进程没有结束则继续往后执行
    printf("end\n"); // 不会打印的
    return 0;
}


#include <unistd.h>
unsigned int alarm(unsigned int seconds);// 时钟信号
// 默认动作是结束程序,seconds为倒计时


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char* argv[]){
    printf("begin\n");
    alarm(5); // 5s后发送结束进程信号给本进程
    while (1)
    {
    }
    printf("end\n");
    return 0;
}

原本应该死循环的程序,5s后死亡了

  • 信号的自定义处理方式

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

signum:要自定义信号的序号
handler:处理动作 sighandler_t 就是函数指针类型。注意上面typedef定义了sighandler_t必须是返回值为void,形参为int的函数
如果成功:返回该之前的sighandler_t,如果失败返回SIG_ERR的宏
SIGKILL 和 SIGSTOP 不能使用自定义处理方式处理信号


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

void handler(int sig){
    printf("catched ctrl+c signal\n");
}

int main(int argc, char* argv[]){
    // 只是设置SIGINT的处理函数为handler函数,并不会调用该函数
    signal(SIGINT, handler);
    printf("begin\n");
    while (1)
    {
    }
    printf("end\n");
    return 0;
}

上面提到过程序运行时ctrl+c发送的是SIGINT信号
我的自定函数的意思就是如果进程运行时使用ctrl+c会输出字符串不会执行原本的终止函数

  • 程序设计
    • 售票员关门之后发送开车信号给司机 SIGUSR1(没有任何意义的信号,用于用户自定义)
    • 接受拦车信号:开车时有人拦车 SIGINT(ctrl+c)
    • 发送开门信号:司机发送开门信号给售票员(SIGUSR2)
    • 司机
      • 接收开车信号
      • 发送开门信号
      • 接受拦车信号
    • 售票员
      • 发送开车信号
      • 接受开门信号

#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

pid_t g_pid; // 保留进程号

void driver(int sig){
    if (sig == SIGINT)
    {
        printf("有人拦车,司机停车……\n");
        sleep(1);
        kill(g_pid, SIGUSR2); // 通知售票员开门
    } else if (SIGUSR1 == sig)
    {
        printf("司机开车……\n");
    }
}
void seller(int sig){
    if (sig == SIGUSR2)
    {
        printf("售票员开门……\n");
        sleep(1);
        printf("乘客买票上车\n");
        sleep(1);
        printf("关好门……\n");
        kill(getppid(), SIGUSR1); // 通知司机开车
    }
}

int main(int argc, char* argv[]){
    g_pid = fork();
    if (g_pid > 0) // 司机
    {
        signal(SIGINT, driver);
        signal(SIGUSR1, driver);
        printf("司机:准备开车……\n");
        while (1)
        {
            pause();
            //挂起程序,等待信号,信号来了之后处理信号接着执行程序
        }
    } else if (g_pid == 0) // 售票员
    {
        // 忽略拦车信号
        signal(SIGINT, SIG_IGN);
        // 开门信号
        signal(SIGUSR2, seller);
        printf("售票员:关好门,开始发车……\n");
        kill(getppid(), SIGUSR1); // 发送开车信号给司机
        sleep(1);
        while (1)
        {
            pause(); // 等待司机信号
        }
    } else {
        perror("fork ");
        return -1;
    }
    return 0;
}

使用ctrl+c进行模拟拦车

  • 信号屏蔽

#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

void handler(int sig){
    if (sig == SIGINT) // ctrl + c
    {
        printf("&&&&&&&&&&&&&&&&&&&&&\n");
        int num = 5;
        while (num--)
        {
            printf("this signal in while %d\n", num);
            sleep(1);
        }
    }else if (sig == SIGQUIT){ // ctrl + /
        printf("^_^\n");
    }

}

int main(int argc, char* argv[]){

    signal(SIGINT, handler);
    signal(SIGQUIT, handler);
    while (1)// 为了防止程序结束
    {
        printf("int main while\n");
        sleep(1);
    }

    return 0;
}

当上述程序运行过程中,你多次按ctrl+c会发现信号是一个一个处理的
得出结论:在处理A信号的过程中再次发生了A信号,那么新的A信号会被屏蔽掉,不是信号被丢掉而是被阻塞了,在之前的A信号处理完之后新的阻塞的A信号才会被处理,并且如果阻塞的A信号有多个,最终也只会处理一次(阻塞了10个A信号,只会执行1次)
假如在处理A信号的过程中,发生B信号,会暂停A信号的处理,先去处理B信号,处理完B信号后再接着处理之前的A信号

  • 很明显上述的信号处理方式有着很大问题
    • 可能原本A信号马上就处理完了,但是B信号一来A信号就必须终止
    • 可能A信号刚分配到关键资源,B信号一来资源被释放了
    • 可能A信号读文件,文件指针移动到100的位置;B信号一来也读文件,文件指针被移动到200;等再次处理A信号时文件指针就不是100了

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int rt_sigprocmask(int how, const kernel_sigset_t *set,kernel_sigset_t *oldset, size_t sigsetsize);
int sigprocmask(int how, const old_kernel_sigset_t *set, old_kernel_sigset_t *oldset);

 #include <signal.h>
int sigemptyset(sigset_t *set);             // 清空集合
int sigfillset(sigset_t *set);              // 将系统支持的信号全部填入集合
int sigaddset(sigset_t *set, int signum);   // 添加信号到集合中
int sigdelset(sigset_t *set, int signum);   // 删除集合中的信号
int sigismember(const sigset_t *set, int signum); // 查看某个信号是否在集合中

使用sigprocmask来屏蔽信号,设置信号处理过程中需要屏蔽的信号
set 是一个信号集合,用放置需要屏蔽/需要解除拼屏蔽的信号
how 就是对set集合中信号的操作:SIG_BLOCK(阻塞,set集合中的信号发生的时候将会不起作用),SIG_UNBLOCK(解除阻塞),SIG_SETMASK(使用set中信号的状态设置系统中对应信号的状态)
oldset 是保留这些信号之前的处理状态,如果不关心之前的状态置为NULL就行


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

void handler(int sig){
    if (sig == SIGINT) // ctrl + c
    {
        // 屏蔽其他信号
        sigset_t set;
        sigset_t oldset;
        // 清空集合
        sigemptyset(&set);// 清空set
        sigemptyset(&oldset);
        // 不想被任何信号干扰
        sigfillset(&set);// 将操作系统所有信号都添加到set中
        // sigprocmask(SIG_BLOCK, &set, NULL); // 因为我们不关注之前的状态,所以最后设为NULL
        sigprocmask(SIG_BLOCK, &set, &oldset); // 因为还原之前的状态,所要用oldset保存
        printf("&&&&&&&&&&&&&&&&&&&&&\n");
        int num = 5;
        while (num--)
        {
            printf("this signal in while %d\n", num);
            sleep(1);
        }
        // 解除阻塞
        sigprocmask(SIG_SETMASK, &oldset, NULL);
    }else if (sig == SIGQUIT){ // ctrl + /
        printf("^_^\n");
    }
}

int main(int argc, char* argv[]){
    signal(SIGINT, handler);
    signal(SIGQUIT, handler);
    while (1)// 为了防止程序结束
    {
        printf("int main while \n");
        sleep(1);
    }
    return 0;
}

如果屏蔽的信号发生了多次,解除屏蔽后只会被处理一次
你也可以多次调用sigaddset方法添加信号到集合中


#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

 struct sigaction {
    void     (*sa_handler)(int);                         // 处理函数的函数指针
    void     (*sa_sigaction)(int, siginfo_t *, void *);  
    sigset_t   sa_mask;                                  // 信号屏蔽集
    int        sa_flags;
    void     (*sa_restorer)(void);                       // 留着以后使用的
};


signum是需要特殊处理的信号,sigaction结构体设置信号的处理方式和需要屏蔽的信号集,oldact保留之前的处理方式和之前的信号集
sa_flags 用来设置信号处理的其他相关操作,下列的数值可用:SA_RESETHAND(当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL),SA_RESTART(如果信号中断了进程的某个系统调用,则系统自动启动该系统调用),SA_NODEFER (一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号)


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

void handler(int sig){
    if(sig == SIGINT){
        printf("$$$$$$$$$$$\n");
        int num = 5;
        while (num--)
        {
            printf("in ctrl + c %d while\n", num);
            sleep(1);
        }
    }
}

int main(int argc, char* argv[]){
    struct sigaction act;
    struct sigaction oldact;

    act.sa_handler = handler; // 信号的处理函数
    sigemptyset(&act.sa_mask); // 清空信号集
    sigfillset(&act.sa_mask); // 填充信号集
    sigaction(SIGINT, &act, &oldact);
    while (1)
    {
        printf("in main function\n");
    }
    return 0;
}

上面代码就实现了信号屏蔽
也就是说sigaction()可以同时设置信号的处理函数和信号屏蔽的信号集

线程

  • 线程:从属于进程的并发执行流程
    • 创建线程不对进程拷贝,和进程共用所有资源
    • 也将线程称之为轻量级的进程
    • 一个进程可以创建多个线程,多个线程不和进程分离,所有线程共享资源
    • 若进程结束,那么所有线程全部结束

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

参数1:线程变量的指针,获得创建线程的标识,方便操作线程
参数2:设置线程属性,例如优先级,可以为NULL
参数3:设置线程执行的函数,作为线程要执行的代码
参数4:传递执行函数的参数
如果设置成功返回0,同时自动执行函数;失败返回错误编号,并且线程标识为未定义
编译时记得加上 -pthread,链接库函数

  • 将创建线程的进程称之为主线程
    • 若一个进程没有创建线程,既可以称之为进程,也可以称之为线程
    • CPU最小的调度单位为线程

#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <pthread.h>
#include <unistd.h>

void *threadFunc(void *arg){
    char *pData = (char *)arg;
    while (1)
    {
        printf("in thread : %s \n", pData);
        sleep(1);
    }
}

void *inThread(void *arg){
    long data = (long)arg;// 这里理论上是转为int的,但是linux下void*不能转为int,所以转为long
    while (1)
    {
        printf("this thread num is %ld\n", arg);
        sleep(1);
    }
}

int main(int argc, char* argv[]){
    pthread_t th; // 线程
    pthread_t t[5];
    int ret = -1;
    char data[20] = {'\0'};
    strcpy(data, "hello_world");
    printf("data = %s\n", data);
    ret = pthread_create(&th, NULL, threadFunc, data);
    if ( 0 != ret)
    {
        printf("create thread error\n");
        return -1;
    }
    for (int i = 0; i < 5; i++)
    {
        ret = pthread_create(t+i, NULL, inThread, (void *)i);
        if ( 0 != ret)
        {
            printf("create thread error\n");
            return -1;
        }
    }
    int num = 5;
    while (num--) // 如果不加循环,线程还没执行完主进程就结束了,线程就直接没了
    {
        printf("in main func\n");
        sleep(1);
    }// num = 0的时候主线程死亡,其与线程也就死了
    return 0;
}

  • 线程的作用

    • 创建多个并发执行流程,同时处理多个事务
    • 当程序中的某个模块执行速度跟不上其他模块的时候,可以在这个模块中创建多个线程,并发处理事务
      • 该模块若创建多个线程,则该模块在CPU中获得总体时间增多
      • 相对的其他模块获得的时间减少
      • 从而达到整体速率匹配的作用
  • 线程的注意事项:线程所有资源共享,存在资源竞争的问题


  • 线程同步之线程锁:为的就是解决上述的资源竞争问题

#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <pthread.h>
#include <unistd.h>

int g_iData = 0;

void *fun(void *arg){

    while (1)
    {
        g_iData ++;
        printf("thread while : %d\n", g_iData);
    }
}

int main(int argc, char* argv[]){
    pthread_t th; // 线程
    pthread_create(&th, NULL, fun, NULL);
    while (1)
    {
        g_iData ++;
        printf("main while : %d\n", g_iData);
    }
    return 0;
}

因为没有使用sleep所以执行特别快,所以运行一会就立马停下
我这台机器上的输出 main while : 1 main while : 3 main while : 4 main while : 5 main while : 6 main while : 7 main while : 8 main while : 9 thread while : 2 thread while : 11
很明显和我们理想上的输出不一样:上面的g_iData+1写回了内存,但是没有进行输出,导致输出的顺序混乱
由于这台电脑的性能还行没有出现另一种问题:同一个数字可能出现两遍,主线程将g_iData+1了之后值还未写入内存,就切换到线程,导致同样的数字出现两遍


#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

使用上锁机制,封装原子操作(不可分开执行)


pthread_mutex_t mutex; // 互斥锁

void *fun(void *arg){
    while (1)
    {
        pthread_mutex_lock(&mutex);
        g_iData ++;
        printf("thread while : %d\n", g_iData);
        pthread_mutex_unlock(&mutex);
    }
}

  • 线程同步:解决多线程间资源竞争的操作,保证各自执行的结果正确,避免线程在资源竞争时产生的数据混乱

    • 上面的线程锁,使用互斥量量进行原子操作,使得操作不能被打断
    • 使用信号量实现线程间的同步:将所有共享资源数据化
      • 使用数字(1,2,3…)来记录资源的数量
      • 使用资源之前,检查信号量的值
      • 当数字大于0时,资源可用
      • 当数字小于0时,资源不可用,进程阻塞等待
  • 生产者与消费者问题

    • 有8个资源
    • 生产者可以生产资源
    • 消费者会消耗资源
    • 生产者生产资源的时候消费者不能
    • 消费者消费的生活生产者不能生产
    • 有多个消费者和多个生产者
    • 一个消费者只能使用一个资源
    • 可以想象成餐馆吃饭,8个资源是8个盘子,生产者是厨师,消费者是食客。空盘子要生产者生产,
  • 解决方法:

    • 一开始 ———— 生产者信号量:8个资源;消费者:0资源
    • 生产者:首先检查空盘子数量,如果空盘子数量大于0,表示有生产者可以生产;如果有食物的盘子大于0,表示消费者可以消费

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);//初始化无名信号量

int sem_wait(sem_t *sem);
//该函数首先检查sem是否大于0,大于0表示信号量,因此将sem的值减1;若值等于0,则表示没有资源可用,进入阻塞
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

int sem_post(sem_t *sem);
// 将信号量sem的值+1

sem 定义的信号量的地址,pshared设置信号量用于线程间同步或进程间通信,value设置信号量的值
pshared:值为0时,用于线程间同步;值非0,信号量同于进程间通信


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <time.h>
#include <stdlib.h>

#define PRO_NUM 5 // 厨师
#define CUS_NUM 15 // 消费者
#define DISH_NUM 10 // 盘子数目

char g_cDish[DISH_NUM] = {'\0'};

sem_t empty; // 空盘子数目 信号量
sem_t full;  // 有实食物的盘子数目 信号量

int g_iProNum = 0; // 表示生产者应该往哪个盘子中生产食物

void *product(void *arg) //生产者代码
{
    long num = (long) arg;// void * 在linux 64下无法转为int
    while (1)
    {
        // 检查空盘子数量
        sem_wait(&empty);
        //该函数首先检查empty是否大于0,大于0表示有空盘子可供生产,因此将empty的值减1;若值等于0,则表示没有资源可用,直到消费者消费完后有空盘子
        srand((unsigned int)time(NULL));//设置随机因子
        g_cDish[g_iProNum] = 'A' + rand()%26; // 往空盘子放字符
        printf("第%ld个生产者往第%d个盘子中生产了%c\n", num, g_iProNum, g_cDish[g_iProNum]);
        g_iProNum = (g_iProNum + 1)%DISH_NUM;
        sem_post(&full); // 将full的信号量+1,唤醒所有因full阻塞等待的消费者
        sleep(1);
    }
}

int g_iCusNum = 0; // 消费者应该使用的盘子

void *custom(void *arg){
    long num = (long) arg;
    while (1)
    {
        sem_wait(&full);
        printf("第%ld个消费者吃了第%d个盘子中的%c\n", num, g_iCusNum, g_cDish[g_iCusNum]);
        g_cDish[g_iCusNum] = '\0';
        g_iCusNum = (g_iCusNum + 1) % DISH_NUM;
        sem_post(&empty);
        sleep(1);
    }
}

int main(int argc, char* argv[]){
    memset(g_cDish, '\0', sizeof(g_cDish));
    sem_init(&empty, 0, 8);
    sem_init(&full, 0, 0);
    pthread_t pro[PRO_NUM]; // 生产者线程
    for (int i = 0; i < PRO_NUM; i++)
    {
        pthread_create(pro+i, NULL, product, (void *)i);
    }
    pthread_t cus[CUS_NUM]; // 消费者线程
    for(int i=0; i<CUS_NUM; i++){
        pthread_create(cus+i, NULL, custom, (void *)i);
    }
    while (1)//防止主进程结束
    {
        /* code */
    }
    return 0;
}

上述代码运行有问题,仅供参考,提供思路,草

  • 哲学家就餐问题
    • 圆形桌,五个哲学家,5根筷子
    • 每个哲学家右手边一根筷子,五个哲学家形成圆
    • 每个哲学家吃饭要用两根筷子
    • 如何实现五个人的吃饭问题

哲学家就餐问题

该问题,每个哲学家需要判断左右两边两根筷子信号量是否大于0
如果某哲学家左右两边筷子信号量都大于0,则该哲学家可以吃饭
如果每个哲学家都只占有一根筷子则形成死锁
因此这里我们检查哲学家两边的筷子,如果有一个信号量为0就把两边的筷子都放弃。以此解决死锁问题


#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
int semctl(int semid, int semnum, int cmd, ...); // 操作信号量

int semop(int semid, struct sembuf *sops, size_t nsops);

struct sembuf{
    unsigned short sem_num;  /* 要操作的第几个信号量 */
    short          sem_op;   /* 如何操作 */
    short          sem_flg;  /* 一般设置为0 */
}

获得信号量集合的标识的函数
semget:key表示集合的名字,nsems表示集合中信号量的个数,semflg表示权限
如果成功返回集合的标识符,如果失败返回-1同时设置errno的值
semctl:semid 信号量集合的id,semnum 集合中的第几个信号(从0开始),cmd 操作(是一个宏)
semop:semid 信号量集合的id,sops是一个操作信号量的结构体,nsops是值操作多少个信号量


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <time.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

union semun {
    int              val;
    struct semid_ds *buf;
    unsigned short  *array;
    struct seminfo  *__buf;
};

int semid = 0;
pthread_mutex_t mutex;

void getchopsticks(long num){
    int right = num; // 哲学家右手边的筷子序号
    int left = (num+1)%5; // 哲学家左手边筷子的序号
    sembuf sem[2] = {{left, 1, 0}, {right, 1, 0}}; // 对左右两边筷子的信号量+1
    semop(semid, sem, 2);// 检查指定编号的信号量的值,只要有一个为0,则表示资源不满足,进入阻塞等待
}

void putchopsticks(long num){
    int right = num; // 哲学家右手边的筷子序号
    int left = (num+1)%5; // 哲学家左手边筷子的序号
    sembuf sem[2] = {{left, -1, 0}, {right, -1, 0}}; // 对左右两边筷子的信号量-1
    semop(semid, sem, 2);// 检查指定编号的信号量的值,只要有一个为0,则表示资源不满足,进入阻塞等待
}



void *thinkAndEat(void *arg){
    long num = (long)arg;
    while (1)
    {
        printf("第%ld个哲学家思考中……\n", num);
        sleep(1);
        getchopsticks(num);// 拿起筷子
        printf("第%ld个哲学家获得筷子吃饭……\n", num);
        sleep(1);
        pthread_mutex_lock(&mutex);//锁上
        putchopsticks(num);// 放下筷子
        printf("第%ld个哲学家放下筷子思考……\n", num);
        pthread_mutex_unlock(&mutex);//解锁
        sleep(1);
    }
}

int main(int argc, char* argv[]){
    // 五个筷子的资源可用数集合
    semid = semget(0x1024, 5, IPC_CREAT | 0664);
    if (semid == -1)
    {
        perror("thread set ");
        return -1;
    }
    semun sem; // 使用联合体变量设置值
    sem.val = 1; // 信号量的值
    for(int i=0; i<5; i++){
        semctl(semid, i, SETVAL, sem);
        // 使用联合体变量中的val的值设置信号量
        // SETVAL的宏就是设置值
    }
    pthread_t te[5];//5个哲学家
    for (int i = 0; i < 5; i++)
    {
        pthread_create(te+i, NULL, thinkAndEat, (void *)i);
    }
    while (1)//防止主进程结束 其余线程死亡
    {
        /* code */
    }
    return 0;
}

代码结果有误,仅供参考,草

  • 线程结束:线程处理完了结束或者调用函数结束线程
    • 如果在线程中使用exit()函数将会导致所有线程乃至整个进程全部退出

#include <pthread.h>
void pthread_exit(void *retval); // retval为线程返回值,无值返回用NULL
int pthread_cancel(pthread_t thread); // 结束指定线程,可以结束自己也可以结束别的线程

pthread_cancel 向 thread的线程发送结束请求,默认情况下收到结束请求的线程会自杀

  • 可以在线程中设置不接受结束请求

int pthread_setcancelstate(int state, int *oldstate); // 设置结束请求的状态

state 有两种: PTHREAD_CANCEL_ENABLE(死),PTHREAD_CANCEL_DISABLE(不死)
oldsate 就是保留之前的state状态,不想知道的话直接用NULL就行了

  • 线程返回值

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

会等待指定线程结束,同时回收线程的资源
retval用于接收返回值


#include <stdio.h>
#include <string.h>  
#include <errno.h> //包括errno参数
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <time.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

void *func(void *arg){
    int num = 3;
    while (num--){
        printf("this is in thread \n");
        sleep(1);
        if (num == 0)
        {
            pthread_exit((void *)888); // 返回888,也是一种返回值的方式
        }
    }

    return (void *)666; // 正常结束
}

int main(int argc, char* argv[]){

    pthread_t th;
    pthread_create(&th, NULL, func, NULL);
    void *retval = NULL;
    pthread_join(th, &retval);
    // 等待指定线程的结束并回收线程资源,如果已经结束则马上返回
    printf("in main %ld\n", (long)retval );
    return 0;
}

如果不想阻塞等待线程的返回值,可以使用线程分离

网络编程

  • socket编程
    • socket是对网络底层的一个封装,可以将其看做一个文件
      • 发送数据只需要往socket中写数据即可,就像写一个文件(自动添加ip地址,数据链路等)
      • 接受数据只需要读socket即可,就像读文件(自动去除附加信息,重组切片数据)
    • 需要:对方的ip(找到电脑),对方的端口号(识别应用),本地的ip,本地的端口号

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
// protocol 为使用的协议,不知道type对应的协议的话,设置为0,代表使用默认协议
// 该函数返回值为socket的文件描述���,如果失败返回-1同时设置errno的值
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 如果成功返回0,如果失败返回-1同时设置errno

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
// 将“192.168.9.222” 这种点分十进制字符串转文长整型,用于in_addr

#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
// host to net 本地字节序转换成网络字节序

struct sockaddr{
    uint8_t sa_len;
    unsigned short sa_family; // 地址家族,AF_XXX
    char sa_data[14]; // 14字节协议地址
};

struct sockaddr_in {
    uint8_t sa_len; // 结构体长度
    short int sin_family; // 通信类型
    unsigned short int sin_port; // 端口
    struct in_addr sin_addr; // internet地址
    unsigned char sin_zero[8]; // 未使用的
}; // 一般使用这个结构体,使用的时候要转换成sockaddr,历史原因 无法解释

struct in_addr{
    unsigned long s_addr; // 存4字节的IP地址,使用网络字节顺序
};

NamePurposeMan page
AF_UNIX, AF_LOCALLocal communicationunix(7)
AF_INETIPv4 Internet protocolsip(7)
AF_INET6IPv6 Internet protocolsipv6(7)
AF_IPXIPX - Novell protocols
AF_NETLINKKernel user interface device(7)
AF_X25ITU-T X.25 / ISO-8208 protocolx25(7)
AF_AX25Amateur radio AX.25 protocol
AF_ATMPVCAccess to raw ATM PVCs
AF_APPLETALKAppleTalkddp(7)
AF_PACKETLow level packet interfacepacket(7)
AF_ALGInterface to kernel crypto API

上述是domain的参数选项

NamePurpose
SOCK_STREAM提供序列的,稳定可靠的双工的基于连接的字节流(TCP)
SOCK_DGRAM提供无连接,不可靠的传输(UDP)
SOCK_SEQPACKETProvides a sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed maximum length; a consumer is required to read an entire packet with each input system call
SOCK_RAW提供原始的数据底层封装,需要手动封装数据和解析原始数据
SOCK_RDMProvides a reliable datagram layer that does not guarantee ordering
SOCK_PACKETObsolete and should not be used in new programs; see packet(7)

上述是type的参数选项

socket Tcp通信

  • socket tcp通信
    1. 客户端产生socket(),通过connect()函数链接服务器
    2. 服务器产生socket(),用于处理客户端连接,bind()绑定服务器的某个ip(一个服务器可能有多个ip)和端口,listen()监听是否有客户端连接,如果有使用accept()进行连接
    3. 使用read()和write()进行循环的数据交互

socket Udp通信

  • socket udp通信

    1. 客户端产生socket(),之后立马循环的发送接收,不管是否服务器是否连接到
    2. 服务端产生scoket()之后就绑定服务器的ip(一个服务器可能有多个ip),之后就进行传输接收
  • 客户端写法


#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <arpa/inet.h>

int main(int argc, char **argv){
    int sockfd = -1;
    // 我是本地通信,所以使用AF_LOCAL,当然异地可以使用AF_INET使用IPV4
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("socket create ");
        return -1;
    }

    struct sockaddr_in servAddr;
    servAddr.sin_family = AF_INET; // 本地就是AF_LOCAL 异地是其他例如 AF_INET(IPV4)
    servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    // s_addr 是长整型,因此要将 “192.168.9.221” 这种点分十进制转换,使用inet_addr()
    servAddr.sin_port = htons(8080);
    // 可能本地字节序和网络字节序不同,所以本地字节序要转换成网络字节序
    // 大端字节序和小端字节序的区分
    bzero(servAddr.sin_zero, 8); // 将指定位置开始的8个字节全部置为0

    int ret = connect(sockfd, (struct sockaddr *)&servAddr, sizeof(servAddr));
    if (ret == -1)
    {
        perror("conneect ");
        return -1;
    }
    printf("connect success\n");
    return 0;
}

在本地我启动了tomcat,用于监听8080端口

  • 服务器端写法
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

  • accept函数
    • sockfd:通过该参数监听到的客户端
    • addr:addr保存链接过来的客户端的信息(accept填写)
    • 返回值为新的socket,作为后面客户端和服务器信息交互的手段
    • 如果错误返回-1
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <arpa/inet.h>

int main(int argc, char **argv){
    int sockfd = -1;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("socket create ");
        return -1;
    }
    struct sockaddr_in servAddr;
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    servAddr.sin_port = htons(8080); // 监听端口
    bzero(servAddr.sin_zero, 8);
    // 将socket和要监听的ip及作用的端口绑定在一起
    int ret = bind(sockfd, (struct sockaddr *)&servAddr, sizeof(servAddr));
    if (ret)
    {
        perror("bind");
        return -1;
    }
    ret = listen(sockfd, 20); // 监听的socket,监听队列中20为最多可容纳的客户端的数

    if (ret == -1)
    {
        perror("listen");
        return -1;
    }
    struct sockaddr_in clientAddr;
    socklen_t iLen = sizeof(clientAddr);
    int client_sockfd = -1;
    while (1)
    {
        printf("listen cliening ...\n");
        client_sockfd = accept(sockfd, (struct sockaddr*)&clientAddr, &iLen);
        if (client_sockfd == -1)
        {
            perror("accept");
            continue;
        }
        printf("client sockfd : %d, ip : %s, port : %d\n", client_sockfd, inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
        // inet_ntoa 十六进制ip转为字符串
        // ntohs 网络字节序转为本地字节序
    }

    return 0;
}

同时运行客户端和服务端的cpp文件

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值