学习的课程在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中。
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;
图片源自另一个课程
- 其他读写函数
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。
重入:当信号相应函数在执行过程中同一个信号又产生一次,再次调用可能会破坏上一次调用产生的一些结果。因此如果函数是不可重入的就会产生不可预估的错误。
假设一个进程中只有一个线程的情况(多线程补充内容见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状态等待信号的打断,信号到来并处理完成后继续进入阻塞状态。
漏桶
令牌桶
单一定时器实现复用
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_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(使用前将#替换为@)