【C语言 Linux系统开发 视频课程学习笔记】

2023-05-16

  学习的课程在b站:史上最强最细腻的linux嵌入式C语言学习教程【李慧芹老师】

  感谢李老师!感谢up主!

  本篇博客只是收集一下学习过程中遇到的函数和其他知识点,并不会详细展开。某个函数的具体情况还需通过man手册来进一步了解。

一、I/O

1.1 标准I/O

  • FILE结构体
  • 文件的打开/关闭
    fopen();
    fclose();
    命令ulimit -a能显示系统的一些限制信息,比如一个进程默认最多打开1024个文件。
  • 文件内容的读取/写入
    fgetc();fputc();
    fgets();fputs();
    fread();fwrite();
  • 函数族
    printf();scanf()
  • 错误代码
    全局变量errno
    perror();输出errno代表的错误信息到stderr
    strerror();将errno转换为错误代表的字符串
  • 读取一行
    getline();
  • 文件指针定位函数,缓冲区刷新函数
    fseek();ftell();rewind();fflush();
    fseeko();ftello();

全缓冲:目标stream是文件时,是全缓冲。只有遇到一下三种情况之一时才会将缓冲区内容输出到目标stream中:缓冲区装满调用fflush()主动刷新缓冲区遇到\0
行缓冲:目标stream是stdout、stdin时,是行缓冲。只有遇到\n的时候,才会将缓存区内容输出到目标stream中。
无缓冲:目标是stderr时,是无缓冲。不会缓冲,内容会直接输出到目标stream中。

  • 临时文件
    tmpnam();tmpfile();

1.2 系统调用I/O

  • 文件描述符
    每个进程都有一个自己的数组,该进程打开的每个stream都会在这个数组中占有一格。对于文件而言,格子中保存了一个指向结构体的指针,而该结构体中又保存了指向该文件inode的指针,inode中保存了文件实际在物理内存中占用了哪些block。“文件描述符”就是某个文件在数组中对应的下标值,是一个int值。
    进程开启时数组中默认前三个分别是stdin,stdout,stderr
  • 文件的打开/关闭
    open();close();
  • 文件内容的读取/写入,文件指针定位
    read();write();lseek();
  • 按规定长度截断文件
    truncate();
  • FILE结构体和文件描述符的转换
    fileno();fdopen();
  • 文件共享
      打开同一个文件多次,得到多个指向同一个文件的文件描述符
  • 重定向函数,原子操作
    dup();dup2();
    复制文件描述符,使用当前可用范围内最小的文件描述符作为复制得到的结果。两个文件描述符位置上的指针指向同一个结构体,这个结构体指向文件的inode。
  • 操纵文件描述符,设备I/O控制
      fcntl();ioctl();

1.3 高级IO

  • 有限状态机

  • 非阻塞IO

  • IO多路转接:select
    监视文件描述符,如果发生感兴趣的变化就通知处理。
    select(nfds,readfds,writefds,exceptfds,timeout);
    以事件为单位组织文件描述符集合。比较古老,因此移植性较好。
    阻塞地监视文件描述符,当发生变化时会修改readfds,writefds,exceptfds三个文件描述符集合,保留发生改变的,清除没改变的。

  • IO多路转接:poll
    int poll(fds,nfds,timeout);
    struct pollfd {
      int fd; // file descriptor
      short events; // requested events
      short revents; // returned events
    };
    fds是结构体pollfd的数组起始地址,每个结构体包含一个文件描述符fd,感兴趣的事件event,revent保存实际发生的事件。poll以文件描述符为单位阻塞地等待事件发生。当某个文件描述符发生了指定的event时poll就会返回,并将发生的事情保存在revent中。

  • IO多路转接:epoll
    Linux系统在poll的基础上进一步封装,因此没法移植到其他平台上。poll中的结构体数组在epoll中称为一个epoll file,由内核管理。用户无法直接操作结构体数组,只留出一些接口函数留给用户。
    int epooll_create(int size); 创建一个epoll file。size和epoll中管理的文件描述符个数无关,Linux2.6.8以后就废弃了,只要随便给一个正数就行。返回值为epoll file的文件描述符。
    int epoll_ctl(epfd,op,fd,event); 给fd添加、修改、删除的event。
    int epoll_wait(epfd,events,maxevents,timeout); 从epfd中取最多maxevents个事件,保存在数组events中。

// epoll_wait()的man手册部分包含以下内容:
struct epoll_event { 
uint32_t     events;    /* Epoll events */
epoll_data_t data;      /* User data variable */
};

typedef union epoll_data { // 是一个共用体
void    *ptr;
int      fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
  • select poll epoll对比
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

图片源自另一个课程

  • 其他读写函数
    readv(); 读取一些碎片地址上的数据
    writev(); 往一些碎片地址上写数据
  • 存储映射IO
    mmap();munmap(); 将一个文件的内容映射到当前进程的虚拟内存空间中,作为一块数据段来操作文件的内容。把原本利用文件描述符对文件的操作变成对当前进程的一块内存的操作。映射完成后可以close该文件,并不影响对映射过来的内存的操作。如果最后一个参数选择匿名映射MAP_ANONYMOUS,则可以不依赖于任何文件,而是将一块指定大小的实际内存空间映射到本进程的虚拟内存空间中。

mmap和malloc:mmap可以选择匿名映射而在自己的虚拟内存空间中开辟一块指定大小的空间,作用和malloc类似。而munmap和free类似用于释放一块虚拟内存空间。

利用mmap()可以完成父子进程间的共享内存通信。父进程先mmap匿名映射一块内存到自己的内存空间中,然后fork产生一个子进程,子进程的内存空间中也保留了这种映射关系。

  • 文件锁
    fcntl();
    lockf();
    flock();

    文件锁作用在文件的inode上,因此通过某个fd给文件上的锁可以通过其他fd解锁。

二、文件系统

2.1 目录和文件

  • 获取文件属性
    stat();fstat();lstat();

  • 空洞文件

  • 文件访问权限
    st_mode是一个16位的位图,用于表示文件类型、文件访问权限,以及特殊权限位

  • umask
    防止产生权限过松的文件

  • 文件权限的更改/管理
    chmod、fchmod

  • 粘住位

  • 文件系统
    FAT
    UFS

  • 硬链接,符号链接
    ln指令;link();unlink();

硬链接:在目录文件中添加一行目录项,用另一个文件名去指向同一个文件的inode。不能给分区建立,不能给目录建立。
符号链接:作用和Windows的快捷方式类似。建立一个空文件,它是一个独立的文件,有自己的inode,但不占用block。访问符号链接文件的inode会指向原文件的block。可跨分区,可以给目录建立。

  • utime
    可更改文件的最后读取的时间和最后修改的时间
  • 目录的创建和销毁
    mkdir、rmdir
  • 更改当前工作路径
    chdir、fchdir
  • 分析目录/读取目录内容
    glob();
    opendir();closedir();readdir();rewinddir();

2.2 系统数据文件和信息

  • 用户信息
    /etc/passwd
    getpwuid();getpwnam();
  • 组信息
    /etc/group
    getgrgid();getgrgrnam();
  • 用户密码加密
    /etc/shadow
    getspnam();crypt();
  • 时间函数
    gmtime();localtime();mktime();strftime();
    在这里插入图片描述

2.3 进程环境

  • 进程的终止
    正常终止:
    (1)从main函数返回
    (2)调用exit
      调用终止处理程序(如钩子函数),调用标准I/O清理程序。
    (3)调用_exit或_Exit
      直接结束进程,不会调用钩子函数。
    (4)最后一个线程从其启动例程返回
    (5)最后一个线程调用pthread_exit
    异常终止:
    (1)调用abort
    (2)接到一个信号并终止
    (3)最后一个线程对其取消请求做出响应

  • 钩子函数
    atexit();
    钩子函数用来注册一些函数,当进程正常终止之前会按注册顺序的逆序来调用函数。类似于C++的析构函数。
    在这里插入图片描述

  • 命令行参数的分析
    getopt();getopt_long();

  • 环境变量
    每个进程在创建时都会复制一套环境变量,在当前进程中改变环境变量并不会改变其他进程的环境变量。
    每个终端窗口都相当于一个新的进程,因此在一个终端窗口中改变了环境变量,不会影响到其他终端窗口。
    getenv();setenv();putenv();

  • C程序的存储空间布局


  • 动态库和静态库重名时,会首先链接动态库。
    静态库
    静态库是在编译时就编入程序当中,调用更快,但会使程序膨胀。
    (1)将库的.c源文件生成.o二进制文件:
      gcc -c yyy.c
    (2)由.o二进制文件生成.a库文件,库名为xx:
      ar -cr libxx.a yyy.o
    (3)将库文件和头文件发布到某一路径下:
      cp libxxx.a /usr/local/lib
      cp yyy.h /usr/local/include
    (4)编译程序时,-I指定头文件位置,-L指定库位置,-l指定库名xx:
      gcc -I/usr/local/include -L/usr/local/lib -o main main.o -lxx
      因为/usr/local/include /usr/local/lib属于标准路径,因此可以在gcc时省略路径的指定,只用-lxx指定链接的库名。
    动态库
    动态库是在程序执行时才根据指定位置去查找调用,速度更慢,但不影响程序的大小。(动态库用的更多一些)
    (1)由库的.c源文件生成.so动态库文件:
      gcc -shared -fpic -o libxx.so yyy.c
    (2)将库文件和头文件发布到某一路径下:
      cp libxxx.a /usr/local/lib
      cp yyy.h /usr/local/include
    (3)在/etc/ld.so.conf文件中添加库所在路径/usr/local/lib
    (4)执行/sbin/ldconfig重读/etc/ld.so.conf文件
    (5)编译程序时,-I指定头文件位置,-L指定库位置,-l指定库名xx:
      gcc -I/usr/local/include -L/usr/local/lib -o main main.o -lxx
      因为/usr/local/include /usr/local/lib属于标准路径,因此可以在gcc时省略路径的指定,只用-lxx指定链接的库名。
    (6)查看可执行文件所链接的动态库:ldd main
    (7)非root用户可以将.so动态库文件发布到任意目录下,再通过环境变量添加该路径:
      cp xx.so ~/test/lib
      export LD_LIBRARY_PATH=~/test/lib
    (8)无论是静态库、动态库,当库xx依赖于库zz时,需要同时链接两个库,且被依赖的库zz要放在后面:
      gcc -o main main.o -lxx -lzz
    手工装载库

  • 函数跳转
    setjmp();longjmp();
    sigsetjmp();siglongjmp();如果要从信号处理函数中往外跳就用这两个。

  • 资源的获取和控制
    getrlimit();setrlimit();


三、并发

3.1 进程的基本知识

  • 进程标识符pid
    进程标识符类型pid_t
    ps查看系统中当前进程
    进程号是顺次向下使用的(和文件标识符不同)
  • 进程的产生
    fork();
    fork()意味着duplicate,意味着父子进程几乎一摸一样,比如父子进程的同一个变量所处的虚拟地址也相同,但映射的物理内存位置可能不同(和“写时复制”规则有关)。
    调用fork()后父子进程的区别:fork的返回值不一样;pid不同;ppid不同;未决信号和文件锁不继承;资源利用量清0;
    init进程:1号进程,是所有进程的祖先进程。
    调度器的调度策略来决定哪个进程先运行。

子进程会拷贝父进程的文件描述符表,因此前三个标准输入输出stream是相同的,父子进程打印的内容会输出到同一个终端窗口。如果是其他打开的文件,父子进程的文件描述符指向的是同一个inode,也就是同一个文件。可以利用这一点完成父子进程间的通信。

在调用fork()之前,务必调用fflush()刷新所有stream。否则诸如printf的缓冲区中的内容也复制给子进程。

  vfork();

  • 进程的消亡及释放资源(收尸)
    父进程等待子进程的状态改变。
    wait();
    会使父进程进入阻塞态。
    waitpid();
    可以指定pid来指定收回的子进程。还能通过配置输入参数来变为非阻塞态。
  • exec函数族
    execl();execlp();execle();
    execv();execvp();

    这一族函数的作用:并不是创建一个新进程,pid不变,而是将当前进程印象替换为一个新的进程印象,即用另一个可执行文件来替换当前的,并从另一个可执行文件的开头开始执行。
  • 用户权限及组权限

u+s:文件权限的user的x位显示为s。执行这个文件时,进程的权限会转变为该文件的user权限
g+s:文件权限的group的x位显示为s。执行这个文件时,进程的权限会转变为该文件的group权限

  执行进程时是带着用户信息的,而每个进程带有3份用户\用户组信息,分别是:
    r—real:当前实际的用户\用户组信息;
    e—effective:当前进程中实际起作用的用户\用户组信息;
    s—save:有些系统平台没有这个;
  开始时res三个用户信息相同,都是当前用户。当进程通过exec()将当前进程印象替换为带有u+s权限的一个可执行文件时,e会转变成该文件的user用户(不一定是root用户)的权限。如果是g+s权限的文件,则e会转变成该文件同组用户的权限。这是将user和group权限暂时(仅限于该可执行文件)下放给其他用户的一种办法。
  u+s和g+s的实现
  getuid();geteuid();
  getgid();getegid();
  setuid();setgid();
  seteuid();setegid();
  setreuid();setregid();

  • 解释器文件
    shell在执行一个普通可执行文件时,会把整个文件装载进来。执行一个脚本文件时,在装载过程中看到开头的脚本文件标记#!得知这是一个脚本文件,shell不会把这个文件继续装载进来,而是会将#!后面跟着的比如/bin/bash作为该脚本的解释器文件装载进来。由这个解释器来解释这个文件的全部内容(包括第一行#!/bin/bash)。由于bash将#看作注释,因此它会跳过第一行#!/bin/bash而去解释后面的内容。
    因此,如果第一行改成#!/bin/cat也没问题,会用cat作为解释器来解释该文件的全部内容,也就是将除了#注释的内容外其他内容显示在终端上。

  • system()
    调用shell来解释一条命令。

  • 进程会计
    acct();

  • 进程时间
    times();

  • 守护进程
    会话session,会话标识sid
    终端
    setsid();

    getpgrp();getpgid();setpgid();

setsid()会创建一个新的session。setsid()只能由某个进程组中的非leader进程调用。而父进程创建子进程后,子进程和父进程是一个进程组的,因此setsid()只能由子进程调用。
当子进程调用setsid()后:
  1、该子进程将成为所创建session的leader;
  2、该子进程会脱离原进程组建立新进程组(但没有脱离父子进程关系),并成为新进程组的leader;
  3、而且该子进程会自行脱离控制终端,作为守护进程运行。
由于守护进程会一直运行,因此原本守护进程的父进程不需要wait等待这个守护进程结束。当父进程结束后,守护进程会被pid为1的init进程接管,其ppid变为1。
因此最终守护进程的ppid=1,pgid=sid=pid都是自己,tty=?。

守护进程单实例控制:某个守护进程第一次启动时往/var/run中一个xx.pid文件写入自己的pid号。如果重复尝试启动该守护进程,则会去检查这个文件内的pid号,如果已经存在,则不再重复启动。

  • 系统日志
    syslogd服务
    openlog();syslog();closelog();

    ubuntu20.04输出的日志信息储存在/var/log/syslog中,日志的书写只能通过syslog()来代为写入。
    只有LOG_INFO及以上级别的日志才会被写入,LOG_DEBUG级别的日志会被丢弃。

经过测试,父进程中调用完openlog()后再创建的子进程能直接调用syslog()写日志而不需要重新调用openlog()。如果父进程调用的是openlog("mydaemon",LOG_PID,LOG_DAEMON);,则父子进程在写日志时分别记录的是自己的pid号。父进程如果执行closelog()子进程还是能正常写日志。

3.2 信号

  • 信号的概念
    信号是软件中断。
    信号的响应依赖于中断。

    命令kill -l可以查看系统的标准信号和实时信号。
    标准信号:因为是位图形式保存,因此会发生丢失;多个标准信号到来时响应顺序未定义。
    实时信号:不会丢失,多次信号多次响应;需要排队;响应是有顺序的。

信号会打断阻塞的系统调用。比如查看open的man手册ERRORS部分有一个EINTR错误:如果系统调用open因为某些原因阻塞着,此时会被某些信号打断。并且全局变量errno会被设置为EINTR。

  • 信号的不可靠

  • 可重入函数
    所有的系统调用函数(man手册第二章内容)都是可重入的。一部分库函数也是可重入的,如:memcpy();
    man手册中如果一个函数另外提供了_r后缀的版本,则原本的函数是不可重入的。

重入:当信号相应函数在执行过程中同一个信号又产生一次,再次调用可能会破坏上一次调用产生的一些结果。因此如果函数是不可重入的就会产生不可预估的错误。

  • 信号的响应过程

在这里插入图片描述
假设一个进程中只有一个线程的情况(多线程补充内容见3.3线程部分的“线程与信号”):

内核为每个进程都维护一份mask和pending,每一位都对应一种信号。
mask相当于开关,某一位置1代表该信号有效。
pending用于记录是否有未响应的信号到来,到来时由内核完成置1。
①进程时间片用完,addr保存进程运行位置x。
②调度到该进程,执行mask&pending来判断有哪些已到来的信号需要响应。如果结果为0,则根据addr保存的位置x继续执行进程。
③在进程运行的过程中,某一信号到来pending.3被置1,但信号处理函数不会马上被执行。时间片用完后进入内核等待调度,addr保存位置y。
④调度到该进程,执行mask&pending!=0,则修改addr为sig_handler()入口地址,令mask.3=0,pending.3=0。根据addr转而执行sig_handler()。(mask置0相当于关闭中断)
⑤sig_handler()执行过程中,可能有n个该信号重复到来,使pending.3又被置1。(mask置1相当于打开中断)
⑥sig_handler()执行完毕,将mask.3置1,重新回到内核等待调度。此时pending.3的0/1状态由信号到来的情况而定。
⑦调度到该进程,执行mask&pending,结果的位3如果是0则修改addr为y继续执行进程,如果是1则addr为sig_handler()入口地址,并令mask.3=0,pending.3=0。再次执行信号处理函数。

1.信号从收到到响应有一个不可避免的延迟:
信号到来时由内核将pending对应位置置1,只有进程从内核态进入用户态时才会执行mask&pending运算,判断是否执行对应的信号处理函数。
2.如何忽略掉一个信号?
将mask的对应位置0。
3.标准信号为什么要丢失?
无论在执行mask&pending运算前同一个信号重复到来几次,pending对应位都只能是1,因此这些重复的信号只会执行1次信号处理函数,多余的都丢失了。
4.标准信号的响应没有严格的顺序。
如果mask&pending结果有多位都是1,则对应的信号处理顺序没有严格规定。

  • 信号常用函数
    signal(); 指定当信号signum到来时,执行func函数的内容。
    sigaction();指定某个信号的处理函数,相比于signal()有更丰富的设置。

  kill();给进程发信号
  raise();给当前进程/线程发信号

  alarm();定时给调用的进程发送一个SIGALARM信号,如果设置了多个只有最后一个会起作用。
  setitimer();选择按照现实时间/用户态cpu时间/完整cpu时间来定时,到时间会发出一个信号给调用的进程。因此这个函数函数的定时不会有误差的累积。
  nanosleep();精确到ns级的休眠
  usleep();usleep();精确到us级的休眠
  select();

  pause();阻塞当前进程,等待一个信号来退出pause()
  abort();给当前进程发送一个SIGABRT信号,结束当前进程。人为地使当前进程异常终止。
  system();调用shell来解释一条命令。调用该函数时需要阻塞信号SIGCHLD,忽略信号SIGINT和SIGQUIT。
  sleep();Linux下sleep采用nanosleep()封装。但有些平台是用alarm()+pause()封装。执行多条sleep()时由于alarm()只有最后一条起作用,会使得sleep的时间错乱。

  sigsuspend();信号驱动程序:解除指定信号的阻塞状态并原子化地进入pause状态等待信号的打断,信号到来并处理完成后继续进入阻塞状态。

  漏桶
  令牌桶
   单一定时器实现复用

  • 信号集
    信号集类型:sigset_t
    sigemptyset();
    sigfillset();
    sigaddset();
    sigdelset();
    sigismember();

  • 信号屏蔽字/pending的处理
    sigprocmask();
    阻塞(block)某个信号的意思是接收该信号并保存到pending中,但并不响应该信号。如果阻塞期间有多个该信号到达,则解除阻塞后只会响应一次。

3.3 线程

  • 线程的概念
    main函数所在线程称为main线程,所有线程都是兄弟关系,而非父子关系。
    main函数返回属于进程的正常终止,所有线程都会结束。
    同一个进程的不同线程之间共享文件描述符表。
    线程标识:pthread_t
    pthread_equal();
    pthread_self();
  • 线程的创建
    pthread_create();
  • 线程的3种终止方式
    1、线程从启动例程返回,返回值就是线程的退出码
    2、线程可以被同一进程中的其他线程取消
    3、线程调用pthread_exit()函数

  pthread_exit(); // ~=exit(); 结束调用的线程
  pthread_join(); // ~=wait(); 给指定线程收尸,会阻塞调用的线程。join的目的是回收线程在内核中占用的资源。而线程占用的栈等资源会自动归还给当前进程不需要join回收。join也用于和pthread_exit配合获取线程函数的返回值。

  • 栈的清理(钩子函数)
    pthread_cleanup_push(); // ~=atexit();指定线程退出前要执行的钩子函数。
    pthread_cleanup_pop();可以选择对应钩子函数是否调用。

由于pthread_cleanup_push和pthread_cleanup_pop使用宏定义的,两个合起来才是完整的函数。因此有一个push就必须有一个pop,否则编译时就会报错。钩子函数的执行顺序和钩子函数push的顺序相反,pop和push的对应顺序也是相反的。比如123三个钩子函数用push挂上,最后三个pop分别对应321,钩子函数的执行顺序也是321。

如果把pthread_cleanup_pop放在pthread_exit后面,还没执行到pop就会结束线程所以看不到pop指定的真假值。也能通过编译,但由于默认为真就会去执行对应的钩子函数。

  • 线程的取消
    pthread_cancel();向一个线程发送一个取消请求,然后调用pthread_join()等待为其收尸。
    取消有2种状态:允许和不允许。
    允许又分为:异步cancel,推迟cancel(默认)->推迟至cancel点再响应取消请求。
    cancel点:POSIX定义可能引发阻塞的系统调用是cancel点(如open()函数)。

  pthread_setcancelstate(); 设置是否允许被取消
  pthread_setcanceltype(); 设置取消方式为异步/推迟
  pthread_testcancel(); 函数本身什么都不做,就是一个cancel点
  pthread_detach(); 线程分离。已经detach的线程不能用join收尸。

  • 线程同步:互斥量
    互斥量类型:pthread_mutex_t

  静态初始化方式:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  动态初始化方式:pthread_mutex_init();
  销毁互斥量:pthread_mutex_destroy();

  pthread_mutex_lock();
  pthread_mutex_trylock();
  pthread_mutex_unlock();

  pthread_once(); 指定某个函数只会被执行1次,防止被多个线程重复调用。

  • 线程同步:条件变量
    条件变量类型:pthread_cond_t cond 条件变量会记录一个阻塞在它上的线程集合,broadcast和signal就是根据这个序列去唤醒线程。

  静态初始化方式:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  动态初始化方式:pthread_cond_init();
  销毁条件变量:pthread_cond_destroy();

  pthread_cond_broadcast(); 唤醒所有阻塞在cond的wait或timedwait上的线程。
  pthread_cond_signal(); 唤醒任何一个阻塞在cond的wait或timedwait上的线程。

  pthread_cond_wait(cond,mutex); 使线程主动阻塞在条件变量cond上,等待被broadcast或signal唤醒。有三点需注意:
  1、线程进入wait时会对mutex解锁
  2、当多个阻塞在cond上的线程被唤醒时,会在wait内部去抢夺互斥锁mutex
  3、抢到互斥锁mutex的线程在退出wait前会对mutex加锁

被唤醒时不一定满足进入临界区的条件,比如消费者被生产者唤醒了,但临界区内并没有可消费的产品,则需要重新阻塞。因此wait最好放在一个while中,接触阻塞后要重新去判断是否要进入临界区。如果条件不成立则继续阻塞。

  pthread_cond_timedwait(); 在wait基础上加一个阻塞时限。

  • 线程同步:读写锁
    读写锁类型:pthread_rwlock_t

  pthread_rwlock_init();
  pthread_rwlock_destroy();
  pthread_rwlock_rdlock(); 读锁,共享
  pthread_rwlock_wrlock(); 写锁,互斥
  pthread_rwlock_tryrdlock();
  pthread_rwlock_trywrlock();
  pthread_rwlock_unlock(); 由拿着读写锁的线程进行解锁

  • 线程同步:信号量
    信号量类型:sem_t

  sem_init();
  sem_destroy();

  sem_post(); 给信号量+1
  sem_wait(); 给信号量-1,阻塞完成
  sem_trywait(); 给信号量-1,非阻塞完成
  sem_timedwait();

  • 线程属性
    线程属性类型:pthread_attr_t

  pthread_attr_init();
  pthread_attr_destroy();
  pthread_attr_setstacksize();见init的man手册see also部分

  • 线程同步量的属性
    互斥量属性:
    pthread_mutexattr_init();
    pthread_mutexattr_destroy();
    pthread_mutexattr_getpshared();
    pthread_mutexattr_setpshared();
    clone();
    pthread_mutexattr_gettype();
    pthread_mutexattr_settype();

    条件变量属性:
    pthread_condattr_init();
    pthread_condattr_destroy();

  • 多线程中的IO
    标准IO函数都支持多线程,因此不会遇到输入输出缓冲区内容混插的情况。因为每次使用IO函数都会给缓冲区加锁。getc_unlocked()等带有_unlocked后缀的是没有锁的版本。

  • 线程与信号
    pthread_sigmask(); // ~sigprocmask() 设置线程的mask屏蔽某些信号
    sigwait(); //~pause() 等待一个信号
    pthread_kill(); //~kill() 以线程为单位发信号
    每个线程都维护有自己的mask和pending,进程为单位没有mask只有一个pending。
    线程间发信号记录在线程的pending中,进程间发信号记录在进程的pending中。
    由于内核是以线程为单位调度的,因此当线程从内核态进入用户态时,会用自己的mask和自己的pending以及进程的pending分别进行两次&操作。

  • 线程与fork()
    在POSIX标准中,fork()创建的进程只是复制了调用fork()的线程作为子进程的main线程。

  • openmp线程标准
    openmp官网


四、进程间通信 IPC

4.1 管道

内核提供,单工,自同步机制。

  • 匿名管道
    只能用于亲缘进程间通信。
    pipe();
  • 命名管道
    可以用于非亲缘进程间通信。
    mkfifo();
    只有凑齐了读写双方才会开始工作。

4.2 XSI标准的3种IPC:消息队列、信号数组、共享内存

指令ipcs能查看当前系统中消息队列、信号数组、共享内存三种IPC方式的现存实例。
主动端:先发包的一方
被动端:先收包的一方

  • 消息实例的key_t量
    key_t ftok(const char *pathname, int proj_id);
    随便给定一个实际存在的文件路径,并且通信的两个进程都拿有同样的一个0~255的整数,利用这个函数可以获得相同的key_t量,利用它来找到同一个IPC实例进行通信。
    文件路径可以随便指定,文件是什么类型,权限如何都没关系,但是必须存在。
    这个函数是利用文件inode的唯一性,再和proj_id进行哈希运算得到唯一的key_t。

如果是父子进程间利用IPC通信,key_t可以不用ftok()来获取,而是用宏IPC_PRIVATE来作为key_t的值。这样,父进程先用IPC_PRIVATE作为key_t量调用xxxget()获取key值,再创建的子进程就拥有这个key值。一旦在xxxget()中第一个参数填IPC_PRIVATE就代表是创建实例,第三个参数不用再写IPC_CREAT,但需要指定实例的权限。

  • 消息队列 Message Queues
    双工。
    msgget(); 创建或根据key_t获取一个消息队列的id。创建时需要在第二个参数指定权限。
    msgctl(); 消息队列的控制,比如销毁消息队列

  struct msgbuf {
    long mtype; // 自己规定的消息类型,>0的一个整形
    char data1[1]; // 实际的数据内容
    int data2; // 实际的数据内容
    float data3; // 实际的数据内容
  };

  int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
   ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
  接收时可以指定收取当前消息队列中的第几个包,因此并非严格遵循FIFO。第三个参数msgsz指定的是实际的数据内容的大小,不包括消息类型那部分大小。应该填sizeof(msgbuf)-sizeof(long),但如果填sizeof(msgbuf)也不会有大问题。

即使消息队列的实例还没有创建,进程也可以用msgget()得到一个msg的key,并向其发送数据。这些数据将会被消息队列缓存起来,等到该实例真的被创建之后再一次性发送出去。指令ulimit -a查看POSIX message queues部分就是缓存区的大小。

  • 信号数组 Semaphore Arrays
    原子化地操作1个及以上的信号量,避免死锁的发生。
    semget(); 创建/获取一个实例
    semctl(); 控制实例

  semop(); 操纵使用实例。会阻塞。

  • 共享内存 Shared Memory
    如果是亲缘进程间通信还是mmap更方便,不需要ftok()获取key_t量
    shmget();
    shmctl();

    shmat(); 将开辟的内存映射到进程虚拟内存空间中
    shmdt();

4.3 网络套接字 socket

  • 字节序问题
    大端:低地址处放高字节
    小端:低地址处放高字节

  由于不同主机的字节序可能不同,因此定义了网络字节序作为中间格式,发送方和接收方都要进行字节序的转换。
  主机字节序h:host
  网络字节序n:network
  字节序转换函数(格式:uint32_t/uint64_t h/n+to+n/h+short/long(uint32_t/uint64_t)): htons(),htonl(),ntohs(),ntohl()

  • 对齐
    编译器会将结构体成员对齐来加快读取速度,但这会改变结构体所占空间的大小,和计算得到的大小有差异。因此网络传输时需要禁止编译器的对齐功能。

  • 类型长度问题
    同种类型在不同机器上长度不同,因此规定采用int32_t,uint32_t,int64_t,int8_t,uint8_t之类的数据类型来确定长度。

  • 抓包器的使用

  • 套接字socket
    用于统一运输层协议和应用层数据的格式。
    int socket(int domain, int type, int protocol);
    domain:协议族
    type:传输类型
    protocol:具体协议,写0就是该协议族中支持type的默认协议
    返回值:文件描述符fd。Linux把socket也看作一个文件,因此IO操作也适用(这更适合用在流式socket中,把socket中收到的字节流看成文件中的字节流)。

  • 报式socket
    以结构体形式的数据报发送,要求数据的完整性。可以是多对多的通信。
    选择AF_INET协议族、报式socket和默认协议,则使用IPPROTO_UDP协议。

  1、给socket分配端口:
  int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); 将自己作为接收方的IP地址和端口号port绑定至socket。
  第二个参数sockaddr结构体格式需要根据socket()的domain参数确定。各协议族所用结构体的实际格式不同,可以在bind的man手册DESCRIPTION部分查看不同协议族所要查找的关键字(如AF_INET查阅man 7 ip)。

  2、发送/接收数据报:
  sendto(); 适用于报式。指定发送到的目标IP地址和端口号port。
  recvfrom(); 适用于报式。因为所用协议族可能不同导致结构体不同,因此需要指定当前函数期望接到的数据报结构体的格式和大小。

  3、IP地址点分式<——>二进制转换:
  inet_pton(); 将IPV4和IPV6的地址从点分式的字符串转换为32位/64位的二进制数。在这个函数中自己的IP地址可以用"0.0.0.0"来代替,会自动替换为当前的实际IP地址。
  inet_ntop(); 将二进制数的IP地址转成点分式字符串。

  4、查看网络状态
  netstat -anu查看UDP
  netstat -ant查看TCP

  5、设置socket属性
  getsockopt();
  setsockopt();

  被动端(先运行,先接收):
  1、socket()取得socketfd
  2、bind()给socket分配端口
  3、recvfrom()收/sednto()发
  4、close()关闭socket

  主动端(后运行,先发送):
  1、socket()取得socketfd
  2、bind()给socket分配端口(可省略)
  3、sednto()发/recvfrom()收
  4、close()关闭socket

被动端不能省略bind(),必须要设定主动端知道的预先定好的端口号。被动端通过收到的数据报的sin_addr和sin_port字段得知源IP和源port。
主动端可以省略bind()。因为主动端先发送,因此只要确定目的IP和port就能完成发送工作,在发送函数sednto()中由系统分配并填入一个源IP和源port。
发送端往数据报的sin_addr和sin_port字段填入的是目的IP和port,接收端从数据报的sin_addr和sin_port字段获取的是源IP和port。

  以AF_INET协议族主动端为例:

struct sockaddr_in st; // sockaddr_in是AF_INET所用结构体
st.sin_family = AF_INET; // 所用的协议族
st.sin_port = htons(1888); // 字节序主机转网络,short型
inet_pton(AF_INET,"0.0.0.0",&st.sin_addr.s_addr); // "0.0.0.0"替换为实际IP,转成32位二进制存入st.sin_addr.s_addr
bind(AF_INET,(sockaddr *)&st,sizeof(laddr)); //结构体指针需要强转格式

!!!端口号务必用htons()而不要用htonl()

  • 流式socket
    字节流的形式连续传输。接收时可以看情况一次接多少长度的内容。一对一、点对点的通信。
    选择AF_INET协议族、流式socket和默认协议,则使用IPPROTO_TCP或IPPROTO_SCTP协议。

  listen(); 设置对某个socket监听。第二个参数指定能维持的全连接最大数。

  connect(); 发送一个连接请求。
  accept(); 接受某个连接请求。自带互斥锁,如果多个线程都阻塞在accept的同一个socket上,只有一个能接受连接。

已连接的socket可以被fork创建的子进程复制。因此父进程创建用于连接处理的子进程后,需要关闭连接。

  send(); 发送一个字节流。
  recv(); 接收一个字节流。recv()也可以替换成文件IO函数,把socket看成一个文件,利用IO函数从socket字节流中读取数据。

  S端,被动端(先运行,先接收):
  1、socket()取得socketfd
  2、bind()给socket绑定port和IP
  3、listen()将socket置为可接受连接的状态
  4、accept()接受连接
  5、send()收/recv()发消息
  6、close()关闭socket

  C端,主动端:
  1、socket()取得socketfd
  2、bind()给socket绑定port和IP(可省略)
  3、connect()发送连接
  4、send()收/recv()发消息
  5、close()关闭socket

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【C语言 Linux系统开发 视频课程学习笔记】 的相关文章

  • 尚硅谷大数据技术Spark教程-笔记06【SparkCore(案例实操,电商网站)】

    视频地址 xff1a 尚硅谷大数据Spark教程从入门到精通 哔哩哔哩 bilibili 尚硅谷大数据技术Spark教程 笔记01 SparkCore xff08 概述 快速上手 运行环境 运行架构 xff09 尚硅谷大数据技术Spark教
  • SparkSql简单案例【Dataset,读取数据、创建表、查询数据】

    beer reviews csv package bigdata beer 导入必要的库和类 import org apache spark SparkConf import org apache spark SparkContext im
  • [Linux] Ubuntu配置安装总结

    每次重装新的Ubuntu后 xff0c 都需要配置安装多个应用 xff0c 这里对一些配置进行下总结 以下安装都是在Ubuntu 16 04上进行的 1 Ubuntu安装Google拼音输入法 之前一直使用搜狗拼音 xff0c 但老是崩 x
  • 尚硅谷大数据技术NiFi教程-笔记01【NiFi(基本概念、安装、使用)】

    视频地址 xff1a 尚硅谷大数据NiFi教程 xff08 从部署到开发 xff09 哔哩哔哩 bilibili 尚硅谷大数据技术NiFi教程 笔记01 NiFi xff08 基本概念 安装 使用 xff09 尚硅谷大数据技术NiFi教程
  • 大数据开会记录【NiFi数据集成、AllData数据中台管理系统、RuoYi】

    今天上午和下午开了个小会 xff0c 上午说了一下Nifi xff0c 下午具体说了一下nifi和ruoyi 目录 上午 下午 上午 三个人开会 上次说的挖掘平台 xff0c 您这边是否有技术人员对nifi比较熟悉 xff0c 并且能够将相
  • 大数据周会-本周学习内容总结012

    开会时间 xff1a 2023 05 07 16 00 线下会议 目录 01 es数据同步至mysql 1 1 在es中插入数据后能够同步到mysql中 1 2 修改与删除es中的数据 02 nifi 2 1 Nifi的单机及分布式集群部署
  • 阅读论文文献工具:Zotero

    zotero是开源的文献管理工具 xff0c 可以方便的收集 xff0c 组织 xff0c 引用 xff0c 和共享文献的工具 由安德鲁 w 梅隆基金会 xff0c 斯隆基金会以及美国博物馆和图书馆服务协会资助开发 zotero xff1a
  • Windows11安装docker

    docker官方网站 xff1a Docker Accelerated Containerized Application Development Win11下安装docker win11安装dockerwindow11安装docker小白
  • 光照强度传感器BH1750

    一 芯片介绍 BH1750FVI是一款数字型光强度传感器集成芯片 BH1750的内部由光敏二极管 运算放大器 ADC采集 晶振等组成 结构图 PD二极管通过光生伏特效应将输入光信号转换成电信号 xff0c 经运算放大电路放大后 xff0c
  • VSCode 安装教程(超详细)

    文章目录 VSCode 安装使用教程 xff08 图文版 xff09 那么 xff0c 什么是 IDE 呢 xff1f 目前 xff0c 前端开发主流的 IDE 有以下 5 个下边我们开始玩转 VSCodeVSCode 下载 安装Windo
  • 没有为字符串添加终止符;初始化为‘\0‘

    使用VS2019遇到问题 1 没有为字符串添加终止符 19条消息 消除VS中C6054 可能没有为字符串 34 XXX 34 添加字符串零终止符的警告 wowpH的博客 CSDN博客 可能没有为字符串添加零终止符 2 char a 10 6
  • 安装vue-element-admin报错git clone --mirror -q git://github.com/adobe-webplatform/eve.git C:\Users\Admin

    问题描述 vue element admin项目 执行安装操作时 xff1a 安装依赖 npm install 建议不要直接使用cnpm安装依赖 xff0c 会出现bug xff0c 可以通过如下操作解决npm下载速度慢的问题 由于淘宝 N
  • 添加启动页

    添加启动页 2021 5 7 实验内容 1 为学号后一位同学编写一个简单app 新建一个emptyactivity 在layout的activity main xml中添加如下代码 span class token number 1 spa
  • Python求最大公约数和最小公倍数

    使用辗转相除法求得最大公约数 xff0c 再利用最大公约数求得最小公倍数 辗转相除法求最大公约数 def gcd num1 num2 if num1 num2 61 61 0 return num2 else num1 num2 61 nu
  • matlab实现梯度下降法

    前记 xff1a 大三上学期学了 运筹学 课程 xff0c 里面包含了许多算法和模型 模型就是线性规划及线性规划的对偶理论 xff0c 单纯形法以及它的实际应用 xff1b 整数规划及其解法 xff08 分支定界法 割平面法匈牙利算法 xf
  • 互斥锁、条件变量、信号量浅析

    互斥锁 条件变量 信号量浅析 互斥锁与条件变量 条件变量是为了保证同步 条件变量用在多线程多任务同步的 xff0c 一个线程完成了某一个动作就通过条件变量告诉别的线程 xff0c 别的线程再进行某些动作 xff08 大家都在semtake的
  • Linux中的tty、pts、pty等概念辨析

    Linux中的tty pts pty等概念辨析 基本概念 tty pty pts ptmx tty xff08 终端设备的统称 xff09 xff1a tty一词源于Teletypes xff0c 或teletypewriters xff0
  • 树莓派4B (aarch64) 安装PyTorch 1.8 的可行方案

    树莓派4B aarch64 安装PyTorch 1 8 的可行方案 最终可行方案 试了一堆方案 xff08 源码编译 Fast ai的安装文件等 xff09 之后 xff0c 终于找到一个可行的方案 是在 PyTorch 官方讨论社区的一个
  • Nvidia CUDA初级教程1 CPU体系架构综述

    Nvidia CUDA初级教程1 CPU体系架构综述 视频 xff1a https www bilibili com video BV1kx411m7Fk p 61 2 讲师 xff1a 周斌 本节内容 xff1a 了解现代CPU的架构和性
  • 解决VNC远程连接树莓派,窗口显示不全的问题

    1 vnc连接 xff0c sudo raspi config中7 A5选分辨率就好 或2 通过SSH命令行的方式连接树莓派 xff0c 在命令行输入 xff1a vncserver geometry 1080x1024

随机推荐

  • qt官网下载指导(新手易犯错误)

    一 获取资源 注意这里不要直接百度qt的官网 xff0c 如果直接百度下载会让你注册一个qt的账号 xff0c 并且一般没人下载最新版的qt6 当然也可以下载旧的版本 xff0c 但是由于本人比较愚笨 xff0c 还没有在官网找到qt旧版本
  • AI智能呼叫中心系统,主要应用场景有哪些?

    互联网智能时代 xff0c 销售人员不再每天拨打数百个电话 xff0c 当前呼叫中心电话系统被企业常用在电销行业 金融行业 保险 贷款催收行业等外呼销售行业 由传统的呼叫中心基础上不断的进行升级和改善 xff0c 逐渐演变为人工智能电话外呼
  • ROS:geometry_msgs中 Point32 , Point ,PointStamped 的定义和转换

    ROS geometry msgs中Point32 Point PointStamped 的定义和转换 Point32和Point的定义Point32和Point的转换PointStamped 的定义和转换 本文所用ROS为kinetic版
  • ROS:回调函数不执行

    最近在写一个接收图片消息的节点时 xff0c 遇到了两次回调函数不调用的情况 xff0c 记录一下 循环中有continue被跳过了 具体代码如下 xff1a while ros ok if mat data 61 61 nullptr s
  • Python猜拳游戏

    关键点在于 user input 43 1 3与computer input的值进行比较 xff0c 这样就省掉了对于边界值0和2的单独判断 xff0c 简化了代码 from random import choice def finger
  • ROS:节点发布消息无对应话题名称

    最近在写一个发布里程的节点的时候遇到了 xff0c 发布了消息 xff0c 在ros却没有对应话题情况 通过rostopic list 和 rosnode info 节点名 命令查看 xff0c 均没有对应话题 xff0c 大致代码如下 x
  • 【python安全攻防】python简易端口扫描器

    文章目录 socket套接字optparse模块socket解析主机进行连接获取bannerthreading多线程端口扫描器python nmap端口扫描 对自己看python绝技的一次学习记录 socket套接字 与TCP端口进行交互
  • ROS中geometry_msgs常用消息类型

    官方文档 xff1a geometry msgs Msg Srv Documentation geometry msgs里面所有的消息数据类型 xff1a 节点之间仅支持使用消息通讯 Accel AccelStamped AccelWith
  • 【docker ros】docker 开启自启运行容器中的程序

    前言 xff1a 使用docker大规模部署应用 xff0c 而又需要开机自启 xff0c 自动启动容器中的程序 xff0c 这里以docker ros为例子 xff0c 使用systemctl服务 xff0c 结合shell脚本 xff0
  • 网络协议与网络编程(双电脑串口通讯)

    C C 43 43 网络编程 双电脑串口通讯 在阅读本文前 请确保先查看这篇文章 xff0c 学习有关TCP网编与单电脑信息传输实践 文章目录 C C 43 43 网络编程 双电脑串口通讯 1 让两台电脑连接同一个局域网2 查询IP地址3
  • Linux shell的数组详解

    Linux shell的数组详解 前提变量数组数组声明声明索引数组声明关联数组 数组赋值数组引用数组中常用的变量数组中的其它操作 示例 前提 程序是指 指令 43 数据 的组合 指令 xff1a bash脚本中的代码 xff08 函数 调用
  • cannot find trajectory file at ./examples/trajectory.txt

    slam十四讲 3 7显示运动轨迹的程序 trajectory txt文件路径修改 string trajectory file 61 34 examples trajectory txt 34 改为 string trajectory f
  • slam14讲,李代数库Sophus遇到的问题及解决办法

    首先遇到sophus安装问题是在第4章时 xff0c 高博虽然书上说 我们使用带模板的Sophus库 xff0c 但实际的代码使用的是非模板库 高博在git上给的sophus库文件我在make的时候报错 xff0c 找不到解决办法 xff0
  • PX4添加新的msg和topic

    本人也是初次接触PX4固件没几天 xff0c 学习了msg的一些知识后写下这个以供自己以后参考 一 首先在 PX4 Autopilot msg文件夹下新建一个 msg文件 xff1a 这里我添加的是Data link msg msg 在 m
  • 物理机安装LINUX后的网卡配置问题

    基础材料 xff1a CentOS7 物理服务器 背景说明 xff1a 安装完LINUX操作系统的网卡配置让主机连通网络 xff0c 对于系统管理员来说是再常见不过的事情 xff0c 而且基本上也没什么难度 xff0c 但凡做过系统管理员的
  • PX4添加新的库(lib)

    就在前一两天 xff0c PX4源代码更新了 xff0c 固件库的名称从Firmware变成了PX4 Autopilot 一 首先在lib下新建一个文件夹并添加 cpp hpp和CMakeLists txt 其中 cpp和 hpp是自己写的
  • PX4添加新的应用

    研究了四天怎么添加新的应用程序 xff0c 前几天都尝试着添加 cpp文件的应用程序 xff0c 跟着网上的方法都失败了 xff0c 结果今天试着添加一个 c文件的应用程序居然成功了 先把 c的添加方法写一写 xff0c 以后学会了怎么添加
  • PX4应用程序开机自启动

    修改启动脚本 新版本的PX4固件这些rc文件位置为 xff1a PX4 Autopilot ROMFS px4fmu common init d文件夹中 1 以rc为开头的都是飞控的启动脚本的文件 xff0c 通过这些文件决定那些应用程序应
  • 自由链表(free list)——SGI STL的空间配置器(allocator)第二级配置器维护方法

    参考 xff1a STL源码剖析 以及 STL空间配置器之第二级配置器的free list详解 span class token comment free list的节点 span span class token keyword unio
  • 【C语言 Linux系统开发 视频课程学习笔记】

    学习的课程在b站 xff1a 史上最强最细腻的linux嵌入式C语言学习教程 李慧芹老师 感谢李老师 xff01 感谢up主 xff01 本篇博客只是收集一下学习过程中遇到的函数和其他知识点 xff0c 并不会详细展开 某个函数的具体情况还