前言

# 介绍

01什么是系统编程

  1. hello world为何能输出到屏幕上?
  2. 操作系统是干嘛的,他的主要任务是什么?

金库->银行->办事窗口(客户)
系统编程就是利用系统调用提供的这些接口、或者说函数、去操作磁盘、终端、网络等硬件
在这里插入图片描述
系统调用:system call类比 银行的办事窗口

02系统编程的特点

  1. 无法跨平台:我们选择学习Linux(类比为各家银行都不一样,linux和windows的系统调用都不一致,linux的系统调用是开源的,windows不 )
  2. 速度慢:用户空间到内核空间的切换需要时间
  3. 更加底层:接口更复杂
    在这里插入图片描述

03系统编程课程目录

问你原理性的东西,不会问你那个参数是干什么的。
《 Linux:系统编程》的前置知识有《 Linux操作系统基础》、《C语言程序设计》、《数据结构》
本课程将带你一步一步学会在 Linux操作系统下编程,使用系统底层提供给我们的各种接口和函数,井深入内核,体验系统底层的精妙之处。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
《 Linux E网络编程基础》的前置课程是《 Linux:系统编程》,在本课程中,我们需要重点学习计算网络知识,特别是运输层的TCP与UDP协议,网络层的路由协议与IP协议。在学习了基础的计算机网络知识后,我们会从 socket入手,学习基于TCP和UDP的多种网络通讯模型

在这里插入图片描述
在这里插入图片描述

# 文件与IO

学完本节课程后,同学将掌握文件的打开、关闭、读写,阻塞与非阻塞IO,同步1IO,文件系统,标准IO,流的打开、关闭与读写,控制缓冲,线程安全:对文件加锁等内容

在这里插入图片描述

01 标准库函数与系统调用

在这里插入图片描述

fopen

在这里插入图片描述

fgetc

在这里插入图片描述
File *stream ;就是句柄,就可以叫做上下文

fput

在这里插入图片描述

fclose

在这里插入图片描述
在这里插入图片描述
1.菜鸟驿站(带缓冲区的)
2.一切皆文件,需要实时操作的内容最好直接使用系统调用

全缓冲
行缓冲: stdout是行缓冲
无缓冲:stderr是无缓冲

#include<stdio.h>

int main() {
    int i;
    for (int i = 0; i < 1025; i++) {
        fputc('A', stdout);
        //linux的缓冲区大小可能是1024个,
        //因为这里只有在1025的时候,才会有缓冲区刷新
    }
    while(1){;}

    return 0;
}

02 open/close/read/write

open

在这里插入图片描述
在这里插入图片描述
umask 一般设置为002

close

在这里插入图片描述

read

在这里插入图片描述

write

       #include <unistd.h>

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

阻塞与非阻塞

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

标准IO实现mycat

  • 自行实现
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main (int argc, char ** argv) {
    
    if (argc < 2) {
        printf("Usage : cmd + filename\n");
        exit(1);
    }

    FILE *fp = fopen(argv[1], "r");

    char c;
    
    while ( (c = fgetc(fp))!= EOF) {
        printf("%c", c);
        
    }

    fclose(fp);
    return 0;

标准IO实现mycp

  • 自行实现
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main (int argc, char ** argv) {
    
    if (argc < 3){
        printf("Usage : cmd + srcFilename + dstFilename\n");
        exit(1);
    }

    FILE *fp1 = fopen(argv[1], "r+");
    if (!fp1) {
        perror("open srcFile");
        exit(1);
    }

    FILE *fp2 = fopen(argv[2], "w");
    if (!fp2) {
        perror("open dstFile");
        exit(1);
    }

    char c;
    
    while ( (c = fgetc(fp1))!= EOF) {
        printf("%c", c);
        fputc(c, fp2);
    }

    fclose(fp2);
    fclose(fp1);
    return 0;
}

  • rewind()返回到文件头
  • fseek():SEEK_SET/SEEK_CUR/SEEK_END
NAME
       fgetpos, fseek, fsetpos, ftell, rewind - reposition a stream

SYNOPSIS
       #include <stdio.h>

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

       long ftell(FILE *stream);

       void rewind(FILE *stream);

       int fgetpos(FILE *stream, fpos_t *pos);

       int fsetpos(FILE *stream, const fpos_t *pos);

DESCRIPTION
       The  fseek() function sets the file position indicator for the stream pointed to by
       stream.  The new position, measured in bytes, is obtained by adding offset bytes to
       the  position  specified  by  whence.   If  whence is set to SEEK_SET, SEEK_CUR, or
       SEEK_END, the offset is relative to the start of the file, the current position in‐
       dicator,  or  end-of-file, respectively.  A successful call to the fseek() function
       clears the end-of-file indicator for the stream  and  undoes  any  effects  of  the
       ungetc(3) function on the same stream.
使用移动文件描述符位置,判断文件大小
#include<stdio.h>

int main(int argc, char **argv) {
    FILE *fp = fopen(argv[1], "r");
    if (!fp) {
        perror("open file");
        return 1;
    }

    fseek(fp, 0, SEEK_END);
    printf("Size = %ld", ftell(fp));

    fclose(fp);
    return 0;
}

03 lseek/fcntl/ioctl/mmap

lseek

在这里插入图片描述
在这里插入图片描述

fcntl

在这里插入图片描述

ioctl

重定向流

#include<stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <sys/ioctl.h>
int main(){
    struct winsize size;
    if (!isatty(1)) {
        perror("1 is not tty\n");
        exit(1);
    }

    if (ioctl(1, TIOCGWINSZ,&size) < 0) {
        perror("ioctl");
        exit(1);
    }

    printf("%d rows, %d colums\n", size.ws_row, size.ws_col);

    return 0;

}

mmap

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

04 虚拟文件系统VFS

ext2文件系统

在这里插入图片描述
文件系统中存储的最小单位是块(Block, ー个块究竟多大是在格式化时确定的,例如mke2fs的-b选项可以设定块大小为1024、2048或4096字节。

  • 启动块( Boot Block)

大小就是1KB,由PC标准规定,用来存储磁盘分区信息和启动信息,任何文件系统都不能使用该块

  • 超级块
    在这里插入图片描述
    在这里插入图片描述
  • inode位图(inode Bitmap)
  • inode表(inode Table)
    在这里插入图片描述
    在这里插入图片描述

stat

在这里插入图片描述
在这里插入图片描述

  • stat的返回值只有一个int,但是需要查询的文件属性却很多,用的就是结构体传值这个功能(传入的是一个地址),
The stat structure
       All of these system calls return a stat structure, which contains the following fields:

           struct stat {
               dev_t     st_dev;         /* ID of device containing file */
               ino_t     st_ino;         /* Inode number */
               mode_t    st_mode;        /* File type and mode */
               nlink_t   st_nlink;       /* Number of hard links */
               uid_t     st_uid;         /* User ID of owner */
               gid_t     st_gid;         /* Group ID of owner */
               dev_t     st_rdev;        /* Device ID (if special file) */
               off_t     st_size;        /* Total size, in bytes */
               blksize_t st_blksize;     /* Block size for filesystem I/O */
               blkcnt_t  st_blocks;      /* Number of 512B blocks allocated */

               /* Since Linux 2.6, the kernel supports nanosecond
                  precision for the following timestamp fields.
                  For the details before Linux 2.6, see NOTES. */

               struct timespec st_atim;  /* Time of last access */
               struct timespec st_mtim;  /* Time of last modification */
               struct timespec st_ctim;  /* Time of last status change */

           #define st_atime st_atim.tv_sec      /* Backward compatibility */
           #define st_mtime st_mtim.tv_sec
           #define st_ctime st_ctim.tv_sec
           };

一个stat的使用案例
#include<stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char** argv){
    if (argc < 2) {
        printf("usage : cmd + filename/dirname\n");
        exit(1);
    }

    struct stat st;

    /**
     *这两行代码互换,有什么区别?
     *stat和lstat有什么区别?
     */
    //stat(argv[1], &st);
    lstat(argv[1], &st);

    /*
    if (S_ISDIR(st.st_mode)) {
        printf("directory\n");
    } else {
        printf("other file type\n");
    }
    */
    
    /**
     *使用stat族函数,可以获取文件的详细信息,
     *进一步得到自己想要的操作
     *此处就是通过stat解析之后,判断文件的类型
     */
    switch(st.st_mode & S_IFMT) {
        case S_IFREG:
            printf("regular file\n");
            break;
        case S_IFDIR:
            printf("directory\n");
            break;
        case S_IFCHR:
            printf("charactor device\n");
            break;
        default:
        printf("other file type\n");
    }
    return 0;
}

opendir(3)/readdir(3)/closedir(3)

在这里插入图片描述

VFS

Linux支持各种各样的文件系统格式,然而这些文件系统都可以 mount到某个目录下,使我们看到一个统一的目录树,各种文件系统上的目录和文件我们用ls命令看起来是一样的,读写操作用起来也都是一样的,这是怎么做到的呢? Linux内核在各种不同的文件系统格式之上做了一个抽象层,使得文件、目录、读写访问等概念成为抽象层的概念,因此各种文件系统看起来用起来都一样,这个抽象层称为虚拟文件系统(VFS, Virtualfilesystem)
在这里插入图片描述

dup 和 dup2

在这里插入图片描述

实现ls -al

# 进程

01. 进程控制块PCB

task struct结构体:ps aux

  • 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数
  • 进程的状态,有运行、挂起、停止、僵尸等状态。
  • 进程切换时需要保存和恢复的一些GPU寄存器。
  • 描述虚拟地址空间的信息。
  • 描述控制终端的信息。
  • 当前工作目录( Current Working Directory)
  • umask掩码。
  • 文件描述符表,包含很多指向file结构体的指针。
  • 和信号相关的信息。
  • 用户id和组id.
  • 控制终端、 Session和进程组。
  • 进程可以使用的资源上限( Resource Limit)。

02. 进程控制fork

pstree

  • fork的作用是根据一个现有的进程复制出一个新进程,原来的进程称为父进程( ParentProcess),新进程称为子进程( Child Process)。系统中同时运行着很多进程,这些进程都是从最初只有一个进程开始一个ー个复制出来的
  • 在Shel下输入命令可以运行一个程序,是因为She进程在读取用户输入的命令之后会调用fork复制出一个新的 Shel li进程。
    在这里插入图片描述
    在这里插入图片描述
    #include <sys/types.h>
    #include <unistd.h>

pid_t fork(void);

在这里插入图片描述
在这里插入图片描述

example1

/**forkOpt.c 
 *注意思考fork的作用
 */
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    char *message;
    int n;
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed");
        exit(1);
    } else if (pid == 0) {
        //sprintf(message,"This is the child, pid = %d\n", pid);
        message = "child processs";
        n = 6;
    } else {
        //sprintf(message,"This is the parent, pid = %d\n", pid);
        message = "parent processs";
        n = 3;
    }
    for (;n > 0; n--) {
        printf("%s, n = %d\n", message, n); 
        sleep(1);
    }

    return 0;
}

  • 运行结果如下:注意思考,子父进程调度顺序?
process % ./a.out                                                                                                        [0]
parent processs, n = 3
child processs, n = 6
parent processs, n = 2
child processs, n = 5
parent processs, n = 1
child processs, n = 4
child processs, n = 3
dhj@DESKTOP-NA5RGM7 process % child processs, n = 2                                                                                          [0]
child processs, n = 1

example:思考父子进程的关系,父进程死后,子进程怎么办?

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

int main() {
    char *message;
    int n;
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed");
        exit(1);
    } else if (pid == 0) {
        n = 6;
        for (;n > 0; n--) {
            printf("\033[31;47mc_pid self\033[0m = %d, parent pid = %d\n", getpid(), getppid()); 
            sleep(1);
        }
    } else {
        n = 3;
        for (;n > 0; n--) {
            printf("p_pid self = %d, parent pid = %d\n", getpid(), getppid()); 
            sleep(1);
        }
    }

    return 0;
}

在这里插入图片描述

example3:创建10个子进程,并打印他们的pid和ppid

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

int main() {
    int i;
    for (i = 0; i < 100; i++) {
        pid_t pid = fork();
        
        if (pid < 0) {
            perror("fork");
            exit(1);
        } 
        if (pid == 0) {
            printf("\033[31;47mchild[%d]\033[0m, self = %d, parent = %d\n", i, getpid(), getppid()); 
            sleep(1);
            break;
        }
    }

    return 0;
}
  • 在不同的平台运行,父子进程的调度顺序确实不一样。
  • 运行结果1:(这是WSL平台的
    在这里插入图片描述
  • 运行结果2:(这是Ubuntu20.04 平台的
    在这里插入图片描述

gdb如何调试多进程?

  • 挖坑

03. exec函数族

exec函数族

  • 当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
    在这里插入图片描述

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值

  • 带有字母l(表示list)的exec函数要求将新程序的每个命令行参数都当作一个参数传给它,命令行参数的个数是可变的,最后一个可变参数应该是NULL,起 sentinel的作用。

  • 对于带有字母v(表示 vector)的函数,则应该先构造一个指向各参数的指针数组,然后将该数组的首地址当作参数传给它,数组中的最后一个指针也应该是NULL,像main函数的argv参数或者环境变量表一样。

  • 不带字母p(表示path)的exec函数第一个参数必须是程序的相对路径或绝对路径,例如"/bin/ls"或"./a.out"。

  • 对于带字母p的函数:如果参数中包含/,则将其视为路径名。否则视为不带路径的程序名,在PATH环境变量的目录列表中搜索这个程序。

  • 对于以e(表示 environment)结尾的exec函数,可以把一份新的环境变量表传给其他exec函数仍使用当前的环境变量表执行新程序

example:用exec族函数调用命令
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>

int main() {
    execlp("ls", "", "-a", "-l", NULL);//第二个参数没有起作用,此处留空了
    perror("exex");
    exit(1);

    return 0;
}

example:实现流的重定向
  • 此处要做的是:1.先实现一个将输入的小写字母转换为大写字母
  • 2.再实现一个将程序的输入和输出重定向到指定文件中去,再调用1.实现的程序,从1个文件中读取,再输出到另一个文件
callback.c :重定向输入输出,用execl调用自己程序执行
#include<stdio.h>
#include<sys/types.h>
#include<fcntl.h>
#include<stdlib.h>
#include<errno.h>
#include<unistd.h>

int main(int argc,char **argv) {
    if(argc != 3) {
        printf("Usage:cmd + inputfile + outputfile\n");
        exit(1);
    }

    int fd = open(argv[1], O_RDONLY);
    if (fd < 0) {
        perror("open inputfile");
        exit(1);
    }
    dup2(fd, 0);//标准输入 重定向 到inputfile
    close(fd);

    fd = open(argv[2], O_WRONLY | O_CREAT, 0644);
    if (fd < 0) {
        perror("open outputfile");
        exit(1);
    }
    dup2(fd, 1);//标准输出 重定向 到outputfile
	close(fd);
	
	//execl("/bin/ls", "/bin/ls", "-a", "-l", NULL);
    execl("./upper", "./upper", NULL);//调用下面编译生成的upper可执行文件
    perror("exec");
    
    exit(0);
}

upper.c :编译只有得到upper可执行文件,在上个程序中调用
#include<stdio.h>
#include<ctype.h>
int main() {
    int ch;
    while((ch = getchar()) != EOF) {
        putchar(toupper(ch));
    }
    return 0;
}

环境变量

在这里插入图片描述

#include<stdio.h>

/*
*循环打印环境变量
*/

int main(void) {
    extern char **environ;

    for(int i = 0;environ[i];i++){
        printf("%s\n",environ[i]);
    }
    
    return 0;
}

  • 思考:下面修改环境变量会一直生效吗?
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main(void) {
    /*
    extern char **environ;

    for(int i = 0;environ[i];i++){
        printf("%s\n",environ[i]);
    }
    */
    printf("path value = [%s]\n", getenv("PATH"));
    setenv("PATH", "hell", 1);
    printf("path value = [%s]\n", getenv("PATH"));
    return 0;
}

04.wait和waitpid函数

  • 一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。

  • 父进程可以调用wait或 waitpid获取这些信息,然后彻底清除掉这个进程。
    在这里插入图片描述

  • 例如:一个进程的退出状态可以在SheI中用特殊变量$?查看,因为 She l I是它的父进程,当它终止时SheI调用wait或 waitpid得到它的退出状态同时彻底清除掉这个进程。

zomb.c制造僵尸:编译运行,用ps -u查看僵尸进程

在这里插入图片描述

  • 当你用ctrl+c强制停止,你会发现,这两个都被收尸了;a.out是被调用a.out的bash先收尸的,然后子进程就变成了孤儿僵尸,被1号进程(孤儿院)收尸了。
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(1);
    }
    if (pid) {
        while (1) 
            sleep(1);
    } else {
        exit(3);
    }
    return 0;
}

example:使用waitpid判断子进程结束的状态

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

int main() {

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(1);
    }
    if (pid == 0) {
        int n = 5;
        while (n > 0) {
            printf("this is child process\n");
            sleep(1);
            n--;
        }
        exit(4);
    } else {
        int stat_val;
        waitpid(pid, &stat_val, 0);
        if (WIFEXITED(stat_val)) {
            printf("Child exited with code %d\n", WEXITSTATUS(stat_val));
        } else if(WIFSIGNALED(stat_val)) {
            printf("CHild terminated abnormally, signal %d\n", WEXITSTATUS(stat_val));
        }
    }
    exit(0);
}

05. 进程间通信

  • 每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到內核缓冲区,进程2再从內核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC, Interprocess Communication)
    在这里插入图片描述

管道pipe

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

example:pipeOpt.c

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

int main () {

    pid_t pid;
    int fd[2];
    int n;
    char buf[20];

    if(pipe(fd) < 0) {
        perror("pipe");
        exit(1);
    }
    /**
     *前面先创建一个管道,后面fork
     *父进程往管道里面写,子进程从管道里读
     */
    pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(1);
    }

    if (pid > 0) {
        close(fd[0]);
        write(fd[1], "hello pipe\n", 11);
        wait(NULL);
    } else {
        close(fd[1]);
        sleep(1);
        n = read(fd[0], buf, 20);
        write(1, buf, n);
    }
    return 0;
}

  • 上面的例子是父进程把文件描述符传给子进程之后父子进程之间通信,也可以父进程fork两次,把文件描述符传给两个子进程,然后两个子进程之间通信,总之需要通过fork传递文件描述符使两个进程都能访问同一管道,它们オ能通信。
  • 使用管道需要注意以下4种特殊情况(假设都是阻塞I/0操作,没有设置 O_NONBLOCK标志)
  1. 如果所有指向管道写端的文件描述符都关闭了,而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。

  2. 如果有指向管道写端的文件描述符没关闭,而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。

  3. 如果所有指向管道读端的文件描述符都关闭了,这时有进程向管道的写端 write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。

  4. 如果有指向管道读端的文件描述符没关闭,而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞, 直到管道中有空位置了才写入数据并返回。

在这里插入图片描述

管道popen和pclose

  • 这两个函数实现的操作是:创建一个管道,forkー个子进程,关闭管道的不使用端,exec一个cmd命令,等待命令终止
    在这里插入图片描述

  • 函数 popen先执行fork,然后调用exec以执行 command,并且返回一个标准I/O文件指针。
    如果type是"r",则文件指针连接到cmd的标准输出。
    如果type是"w",则文件指针连接到cmd的标准输入。

  • 函数pclose关闭标准I/O流,等待命令执行结束,然后返回cmd的终止状态。如果cmd不能被执行,则 pclose返回的终止状态与 shell执行exit一样。

popenOptwrite.c
#include<stdio.h>
#include<stdlib.h>
#include<ctype.h>

int main() {
    FILE *fp = popen("./upper", "w");
    if (!fp) {
        perror("popen");
        exit(1);
    }
    /*用popen打开的,fp占据着标准输出
     * 此fp的内容处理完会输出到终端
     */
    fprintf(fp, "hello world 3 \n ttt survive thrive\n");

    pclose(fp);
    return 0;
}

popenOptread.c
#include<stdio.h>
#include<stdlib.h>
#include<ctype.h>

int main() {
    FILE *fp = popen("cat ./out.txt", "r");
    if (!fp) {
        perror("popen");
        exit(1);
    }

    int c;
    while(~(c = fgetc(fp)))
        putchar(toupper(c));

    pclose(fp);
    return 0;
}

共享内存

  • 进程间通信之共享内存
  • 共享存储允许两个或多个进程共享一给定的存储区。因为数据不需要在客户机和服务器之间复制,所以这是最快的一种IPC。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 其中,key可由fork()生成。pathname必须为调用进程可以访问的。proj_id的bit是否有效。
  • pathname和proj_id共同组成一个key.

shmgetOpt.c

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


int main() {
    key_t key = ftok("./callback.c", 9);

    if (key < 0) {
        perror("ftok");
        exit(1);
    }
    printf("key = ox%x\n", key);
	//创建共享内存,此处IPC_EXCL表示必须自己创建
    int shmid = shmget(key, 20, IPC_CREAT /*| IPC_EXCL*/ | 0666);
    if (shmid < 0) {
        perror("shmget");
        exit(1);
    }
    printf("shmid = %d\n", shmid);


    return 0;
}

在这里插入图片描述

  • 一般应指定addr为0,以便由内核选择地址
    在这里插入图片描述
    在这里插入图片描述

1. shmOpt.c

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


int main() {
    key_t key = ftok("./callback.c", 9);

    if (key < 0) {
        perror("ftok");
        exit(1);
    }
    printf("key = ox%x\n", key);

    int shmid = shmget(key, 20, IPC_CREAT /*| IPC_EXCL*/ | 0666);
    if (shmid < 0) {
        perror("shmget");
        exit(1);
    }
    printf("shmid = %d\n", shmid);
    char *shmp = shmat(shmid, NULL, 0);
    if (shmp < 0) {
        perror("shmat");
        exit(1);
    }
    
    printf("shmp = %p\n", shmp);
    //往共享内存中写数据
    //snprintf(shmp, 20, "hello\n");
    printf("%s", shmp);
    shmdt(shmp);//取消内存映射关系
    
    //如果再此处访问共享内存会怎样?
    //printf("%s", shmp);
    return 0;
}

2.shmOpt.c:体会进程间通信的流程

  • 注意wsl和 ubuntu有细微的区别,注意。
#include<stdio.h>
#include<stdlib.h>
#include<sys/ipc.h>
#include<string.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<strings.h>


int main() {
    key_t key = ftok(".", 10);

    if (key < 0) {
        perror("ftok");
        exit(1);
    }
    printf("key = ox%x\n", key);

    int shmid = shmget(key, 20, IPC_CREAT /*| IPC_EXCL*/ | 0666);
    //也可以指定内存key,如下:
    //int shmid = shmget((key_t)123456, 20, IPC_CREAT /*| IPC_EXCL*/ | 0666);
    if (shmid < 0) {
        perror("shmget");
        exit(1);
    }
    printf("shmid = %d\n", shmid);
    char *shmp = shmat(shmid, NULL, 0);
    if (shmp < 0) {
        perror("shmat");
        exit(1);
    }
    
    printf("shmp = %p\n", shmp);
    
    //往共享内存中写数据
    /*
    int i;
    char *p = shmp;
    for(i = 0; i < 2048; i++) {
        p[i] = 'a';
    }
    p[i] = 'a';
    */
    
    //用来清空共享内存
    bzero(shmp, 20);
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(1);
    }
    if (pid) {
        //parent process
        while(1) {
            scanf("%s",shmp);
            if (!strcmp(shmp, "quit"))
                break;
        }
        wait(NULL);
    } else {
        //child process
        while (1) {
            if(!strcmp(shmp, "quit"))
                break;
            if(*shmp)
                printf("%s\n", shmp);
            
            bzero(shmp, 20);
            sleep(1);
        }
    }
    printf("%s\n", shmp);
    shmdt(shmp);//取消内存映射关系
    
    //如果再此处访问共享内存会怎样?
    //printf("%s", shmp);
    return 0;
}

使用ipcs -m shmid释放共享内存

ipcs                                            [0]

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息      

------------ 共享内存段 --------------
键        shmid      拥有者  权限     字节     连接数  状态      
0x00000000 4          ubuntu     666        1024       0                       
0x0a050002 5          ubuntu     666        20         0                       
0x0a050001 6          ubuntu     666        20         0                       
0x0001e240 7          ubuntu     666        20         2     

~ % ipcrm -m 4                                                                                                                                    
~ % ipcrm -m 5                                                                                                                                    
~ % ipcrm -m 6 
~ % ipcrm -m 7 
~ % ipcs                            [0]

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息      

------------ 共享内存段 --------------
键        shmid      拥有者  权限     字节     连接数  状态      

--------- 信号量数组 -----------
键        semid      拥有者  权限     nsems     

消息队列

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

# 信号

01. 信号的基本概念

  • 用户输入命令,在 Shel l下启动一个前台进程。
  • 用户按下Ctrl-C,这个键盘输入产生一个硬件中断。
  • 如果GPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行,GPU从用户态切换到內核态处理硬件中断。
  • 终端驱动程序将Ctrl-解释成一个 SIGINTT信号,记在该进程的PGB中(也可以说发送了ー个 SIGINT信号给该进程)。
  • 当某个时刻要从内核返回到该进程的用户空间代码继续执行之前,首先处理PGB中记录的信号,发现有一个S1GINT信号待处理,而这个信号的默认处理动作是终止进程,所以直接终止进程而不再返回它的用户空间代码执行。
  • kill -l命令可以察看系统定义的信号列表
  • 这些信号各自在什么条件下产生,默认的处理动作是什么,在 signa l(7)中都有详细说明
  • Term表示终止当前进程,Core表示终止当前进程并且Core Dump,Ign表示忽略该信号,Stop表示停止当前进程,Cont表示继续执行先前停止的进程

02. 如何产生信号

  • 当一个进程要异常终止时,可以选择把进程的用户空间內存数据全部保存到磁盘上,文件名通常是core,这叫做 Core Dump。
  • 在这里插入图片描述
    在这里插入图片描述

03. 如何阻塞信号

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

04. 如何捕捉信号

  • 如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂
    在这里插入图片描述

# 线程

01线程的概念

  • 有些情况需要在一个进程中同时执行多个控制流程,比如实现一个图形界面的下载软件,一方面需要和用户交互,等待和处理用户的鼠标键盘事件,另一方面又需要同时下载多个文件,等待和处理从多个网络主机发来的数据,这些任务都需要一个“等待一处理”的循环,那么如何才能同时进行多项任务呢?
  • 线程( thread):
    操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

由于同一进程的多个线程共享同一地址空间,因此 Text Segment、 Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

  1. 文件描述符表
  2. 每种信号的处理方式
  3. 当前工作目录
  4. 用户id和组id

但有些资源是每个线程各有一份的

  1. 线程id
  2. 上下文,包括各种寄存器的值、程序计数器和栈指针
  3. 栈空间
  4. errno变量
  5. 信号屏蔽字
  6. 调度优先级
    在 Linux上线程函数位于 Iibpthread共享库中,因此在编译时要加上-lpthread选项

02线程控制

在这里插入图片描述

  • 程序设计中的回调函数是为了给后人开门。在早期的程序设计的时候,不知道后来人需要实现什么功能,这一部分就让后来使用的人自己实现,
PTHREAD_CREATE(3)             Linux Programmer's Manual            PTHREAD_CREATE(3)

NAME
       pthread_create - create a new thread

SYNOPSIS
       #include <pthread.h>

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

       Compile and link with -pthread.

  • 这个函数值的第一个参数是结果参数,充当函数返回值作用,类似fork创建进程直接返回pid;
    在这里插入图片描述

createThread.c

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

void printid(char *);
void *thr_fn(void *arg) {
    //todo
    printid(arg);
    return NULL;
}

void printid(char *tip) {
    pid_t pid = getpid();
    pthread_t tid = pthread_self();

    printf("%s pid: %u tid:%u (%p)\n", tip, pid, tid, tid);
   // printf("%s thr_fn=%p\n", tip, thr_fn);
}


int main(){
    
    pthread_t tid;

    int ret = pthread_create(&tid, NULL, thr_fn, "new thread");
    if (ret) {
        printf("create thread err:%s\n", strerror(ret));
        exit(1);
    }
    
    sleep(1);
    printid("main thread");


    return 0;
}

  • 多次运行后会发现,进程pid一直增加,线程tid相差也很大
    在这里插入图片描述
  • 思考:主线程在一个全局变量ntid中保存了新创建的线程的id,如果新创建的线程不调用pthread_self而是直接打印这个ntid,能不能达到同样的效果?
  • 线程如何退出
       The new thread terminates in one of the following ways:

       * It calls pthread_exit(3), specifying an exit status value
         that  is  available to another thread in the same process
         that calls pthread_join(3).

       * It returns from start_routine().  This is  equivalent  to
         calling  pthread_exit(3)  with  the value supplied in the
         return statement.

       * It is canceled (see pthread_cancel(3)).

       * Any of the threads in the process calls exit(3),  or  the
         main  thread  performs a return from main().  This causes
         the termination of all threads in the process.

在这里插入图片描述
在这里插入图片描述

exitThread.c:停止线程

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

void *thr_fn1(void *arg) {
    printf("thread 1 returning\n");
    return (void *) 1;
}
void *thr_fn2(void *arg) {
    printf("thread 2 exiting\n");
    pthread_exit((void *)2);
    return NULL;
}
void *thr_fn3(void *arg) {
    while(1) {
        printf("thread 3 sleeping\n");
        sleep(1);
    }
    return (void *) 1;
}

int main() {
    pthread_t tid;
    void *sts;

    pthread_create(&tid, NULL, thr_fn1, NULL);
    pthread_join(tid, &sts);
    printf("thread 1 exit code %ld\n", (long)sts);

    pthread_create(&tid, NULL, thr_fn2, NULL);
    pthread_join(tid, &sts);
    printf("thread 2 exit code %ld\n", (long)sts);

    pthread_create(&tid, NULL, thr_fn3, NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid, &sts);
    printf("thread 3 exit code %ld\n", (long)sts);


    return 0;
}

  • 运行结果
thread 1 returning
thread 1 exit code 1
thread 2 exiting
thread 2 exit code 2
thread 3 sleeping
thread 3 sleeping
thread 3 sleeping
thread 3 exit code -1

03线程间同步

线程(thread)是允许应用程序并发的执行多个任务的一种机制。一个进程可以有多个线程,如果每个线程执行不同的任务,通过对线程的执行顺序进行控制(调度)就可以实现任务的并发执行。当然了多进程也可以实现任务的并发处理,但是两者之间是有区别的。最大的区别就是拥有的资源不同。进程拥有自己的独立系统资源,而线程没有独立资源,只能和属于同一进程的其他线程共享进程的系统资源。单个资源在多个用户之间共享就会存在一致性的问题,因此需要通过一定的方法来对线程共享资源进行同步。

目前线程间同步主要有互斥量、读写锁、条件变量、自旋锁、屏障等5种方式。

互斥量(mutex):主要用于保护共享数据,确保同一时间只有一个线程访问数据。互斥量从本质上来说是一把锁,在访问共享资源前对互斥量进行加锁,访问完成后释放互斥量(解锁)。对互斥量进行加锁之后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。这样就可以保证每次只有一个线程可以向前执行。

读写锁(reader-writer lock):读写锁也叫做共享互斥锁(shared-exclusive lock),它有三种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。一次只能有一个线程可以占有写模式的读写锁,但是多个线程可以同时战友读模式的读写锁。因此与互斥量相比,读写锁允许更高的并行性。读写锁非常适合对数据结构读的次数远大于写的情况。

条件变量:是线程可用的另一种同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。条件本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种改变,因此互斥量必须在锁住以后才能计算条件。

自旋锁:自旋锁与互斥量类似,但它不是通过休眠使进程阻塞,而是在获取所之前一直处于忙等(自旋)阻塞状态。自旋锁可用于以下情况:锁被持有的时间短,而且线程并不希望在重新调度上花费太多的成本。自旋锁用在非抢占式内核中时是非常有用的,除了提供互斥机制以外,还可以阻塞中断,这样中断处理程序就不会陷入死锁状态。

屏障(barrier):是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行。pthread_join函数就是一种屏障,允许一个线程等待,直到另一个线程退出。
多个线程同时访问共享数据时可能会冲突,这跟前面讲信号时所说的可重入性是同样的问题。比如两个线程都要把某个全局变量增加1,这个操作在某平台需要三条指令完成:

  1. 从内存读变量值到寄存器
  2. 寄存器的值加1
  3. 将寄存器的值写回内存

在这里插入图片描述

cntadd.c:线程之间累加,怎么没有累加呢?
#include<stdio.h>
#include<pthread.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>

int cnt = 0;
void *cntadd(void *arg) {
    int val, i;
    for(i = 0; i < 10; i++){
        val = cnt;
        printf("%p : val = %d cnt = %d\n", pthread_self(),val, cnt);
        cnt = val + 1;
    }
    return NULL;
}
int main(){


    pthread_t tida, tidb;

    pthread_create(&tida, NULL, cntadd, NULL);
    pthread_create(&tidb, NULL, cntadd, NULL);

    pthread_join(tida,NULL);
    pthread_join(tidb,NULL);

    return 0;
}

  • 运行结果:为什么不是10或者20呢?见后面
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

cntadd.c:线程间同步引入锁
#include<stdio.h>
#include<pthread.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>

pthread_mutex_t add_lock = PTHREAD_MUTEX_INITIALIZER;

int cnt;
void *cntadd(void *arg) {
    int val, i;
    for(i = 0; i < 10; i++){
        pthread_mutex_lock(&add_lock);
        val = cnt;
        printf("%p : val = %d cnt = %d\n", pthread_self(),val, cnt);
        cnt = val + 1;
        pthread_mutex_unlock(&add_lock);
    }
    return NULL;
}
int main(){


    pthread_t tida, tidb;

    pthread_create(&tida, NULL, cntadd, NULL);
    pthread_create(&tidb, NULL, cntadd, NULL);

    pthread_join(tida,NULL);
    pthread_join(tidb,NULL);

    return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

condi.c:实现消费者生产者模型,生产者就如压栈,消费者如出栈,类似后进先出
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<unistd.h>
#include<pthread.h>


typedef struct Goods {
    int data;
    struct Goods *next;
} Goods;

Goods *head = NULL;
pthread_mutex_t headlock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t hasGoods = PTHREAD_COND_INITIALIZER;

void *producer(void *arg) {
    Goods *ng;
    while (1) {
        ng = (Goods *)malloc(sizeof(Goods));
        ng->data = rand() % 100;

        pthread_mutex_lock(&headlock);
        ng->next = head;
        head = ng;
        pthread_mutex_unlock(&headlock);
        pthread_cond_signal(&hasGoods);
        printf("produce %d\n", ng->data);

        sleep(rand() % 2);
    }
}

void *consumer(void *arg) {
    Goods *k;
    while(1) {
        pthread_mutex_lock(&headlock);
        if(!head) {
            pthread_cond_wait(&hasGoods, &headlock);
        }
        k = head;
        head = head->next;
        pthread_mutex_unlock(&headlock);
        printf("\033[31;47mconsume\033[0m %d\n", k->data);
        free(k);
        sleep(rand() % 4);
    }
}

int main() {
    srand(time(NULL));
    pthread_t pid,cid;

    pthread_create(&pid, NULL, producer, NULL);
    pthread_create(&cid, NULL, consumer, NULL);

    pthread_join(pid, NULL);
    pthread_join(cid, NULL);

    return 0;
}

  • 运行结果:
    在这里插入图片描述

Semaphore

在这里插入图片描述
在这里插入图片描述

sem.c:生产者消费者模型:类似先进先出
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<unistd.h>
#include<pthread.h>
#include<semaphore.h>

#define NUM 5

int q[NUM];
sem_t blank_number, goods_number;

void *producer(void *arg) {
    int i = 0;
    while (1) {
        sem_wait(&blank_number);
        q[i] = rand() % 100 + 1;
        printf("produce %d\n", q[i]);
        sem_post(&goods_number);
        i = (i + 1) % NUM;
        sleep(rand() % 1);
    }
}

void *consumer(void *arg) {
    int i = 0;
    while(1) {
        sem_wait(&goods_number);
        printf("\033[31;47mconsume\033[0m %d\n", q[i]);
        q[i] = 0;
        sem_post(&blank_number);
        i = (i + 1) % NUM;
        sleep(rand() % 4);
    }
}

int main() {
    srand(time(NULL));
    pthread_t pid,cid;
    
    sem_init(&blank_number, 0, NUM);
    sem_init(&goods_number, 0, 0);

    pthread_create(&pid, NULL, producer, NULL);
    pthread_create(&cid, NULL, consumer, NULL);

    pthread_join(pid, NULL);
    pthread_join(cid, NULL);

    return 0;
}

  • 运行结果:
    在这里插入图片描述

后记

  • 嗨,你好呀!
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐