-
同步和异步、阻塞和非阻塞
1 同步和异步
同步和异步指的是在进行I/O操作完成之前,是否允许其他处理步骤继续执行。
计算机中的I/O操作相对于数据处理操作时十分耗时的。一个简单的I/O操作方式就是启动连接并等待操作完成,但是这样的操作(同步阻塞I/O)在通信过程中会阻塞进程的处理进度。
相应的,可以在启动通信的同时进行其他的处理,并不需要等待I/O操作的完成,这样的操作就被称作是异步I/O。那些依赖于I/O操作执行完成的任务会阻塞等待I/O操作的完成,其他不依赖与I/O操作的任务能够继续执行。同步模型常用的函数接口:
read
,write
,send
,recv
异步模型常用的函数接口:aio_write
,aio_read
1.1 POSIX AIO
在头文件
aio.h
中定义,链接时使用-lrt
函数接口
异步写操作int aio_read(struct aiocb* aiocbp);
异步读操作
int aio_write(struct aiocb* aiocbp);
获取异步操作结果
int aio_return(struct aiocb* aiocbp);
获取异步操作中的错误
int aio_error(struct aiocb* aiocbp);
示例代码: github gist
1.2 Linux AIO
在头文件
libaio.h
中定义,链接时使用-laio
函数接口
需要注意的是aio的函数接口需要借助syscall
进行调用。
创建aio context对象int io_setup(unsigned nr, aio_context_t* ctxp);
销毁aio context对象
int io_destroy(aio_context_t ctx);
提交异步操作
int io_submit(aio_context_t ctx, long nr, struct iocb** iocbpp);
获取异步操作结果
int io_getevents(aio_context_t ctx, long min_nr, long max_nr, io_event* events, struct timespec* timeout);
示例代码:
1.3 POSIX AIO与Linux AIO的区别
On linux, the two AIO implementations are fundamentally different.
The POSIX AIO is a user-level implementation that performs normal blocking I/O in multiple threads, hence giving the illusion that the I/Os are asynchronous. The main reason to do this is that:- it works with any filesystem
- it works (essentially) on any operating system (keep in mind that gnu's libc is portable)
- it works on files with buffering enabled (i.e. no ODIRECT flag set)
The main drawback is that your queue depth (i.e. the number of outstanding operations you can have in practice) is limited by the number of threads you choose to have, which also means that a slow operation on one disk may block an operation going to a different disk. It also affects which I/Os (or how many) is seen by the kernel and the disk scheduler as well.
The kernel AIO (i.e. iosubmit() et.al.) is kernel support for asynchronous I/O operations, where the io requests are actually queued up in the kernel, sorted by whatever disk scheduler you have, presumably some of them are forwarded (in somewhat optimal order one would hope) to the actual disk as asynchronous operations (using TCQ or NCQ). The main restriction with this approach is that not all filesystems work that well or at all with async I/O (and may fall back to blocking semantics), files have to be opened with ODIRECT which comes with a whole lot of other restrictions on the I/O requests. If you fail to open your files with ODIRECT, it may still "work", as in you get the right data back, but it probably isn't done asynchronously, but is falling back to blocking semantics.
Also keep in mind that iosubmit() can actually block on the disk under certain circumstances.在Linux上两种AIO是完全不同的;
POSIX AIO实现在用户层,实际上进行的操作是普通的多线程阻塞操作,表现为I/O操作是异步的,这种AIO的优点是兼容性和可移植性好,缺点是操作队列长度受限于最大线程数量。
Linux AIO是内核提供的AIO函数接口,I/O操作请求的队列在内核中维护,这种AIO的缺点是并不支持所有的文件系统,Linux AIO在某些情况下的磁盘操作是会阻塞的。2 阻塞和非阻塞
阻塞与非阻塞的概念针对的是函数是否会立即返回。
非阻塞模型常与IO复用技术组合使用。
可以通过函数将IO设备设置为非阻塞模式。3 如何理解阻塞非阻塞与同步异步的区别
在处理 IO 的时候,阻塞和非阻塞都是同步 IO。
只有使用了特殊的 API 才是异步 IO。 - it works with any filesystem
-
查看Linux系统的相关信息
查看Linux系统相关信息有助于排查和解决软件和硬件的兼容性问题。系统信息包括硬件信息和软件信息,硬件信息主要有CPU信息、内存信息、PCI信息、USB信息、硬盘信息等等。软件信息主要有系统版本、分区使用状态等等。本文主要介绍了获取当前Linux系统信息的命令。
查看系统相关信息
uname
指令提供了查询系统信息的功能,使用该命令能够快速获取操作系统信息概览。查看内核名称
uname -s
查看处理器类型
uname -p
查看硬件架构
uname -i
查看内核版本
uname -r
查看所有系统信息
uname -a
查看操作系统信息(发行版信息)
cat /etc/os-release cat /proc/version lsb_release -a hostnamectl
查看CPU相关信息
lscpu
指令能够查看当前系统中CPU的详细信息,包括型号、主频、构架、大小端等信息。查看硬盘相关信息
lsblk
指令能够查看块设备(block device)的详细信息,块设备主要指系统中的存储设备如硬盘和闪存。查看PCI设备的相关信息
lspci
指令能够查看PCI设备的信息,PCI设备包括USB、显卡、串口、网卡等其他外围设备。输出树形结果
lspci -t
输出详细信息
lspci -v lspci -vv
查看USB设备的相关信息
lsusb
指令能够查看USBS设备的信息。查看文件系统相关信息
fdisk
命令能够查看和操作linux系统的分区表。查看文件系统信息
fdisk -l
df
命令能够查看分区信息和硬盘使用信息使输出信息更容易理解
df -h
-
Linux常用命令行指令 - top
linux的top命令能够动态显示当前系统的运行状态。它能够显示操作系统的汇总信息和当前系统上的正在运行的进程列表。
统计信息说明
(可以通过配置文件修改通计信息区的显示格式,下文所描述的都是在默认配置下的显示内容)
第1行
- 当前系统时间
- 当前系统的启动时长
- 当前登录的用户数量
- 当前系统的平均负载(分别是1min,5min,10min的平均负载)
这一行信息也可以通过
uptime
和w
指令获得第2行
- 总进程数
- 正在运行的进程数
- 休眠的进程数
- 停止的进程数
- 僵尸进程数
第3行
- us 用户空间占用CPU百分比
- sy 内核空间占用CPU百分比
- ni 用户进程空间内改变过优先级的进程占用CPU百分比
- id 空闲CPU百分比
- wa 等待输入输出的CPU时间百分比
- hi CPU服务于硬件中断所耗费的时间总额
- si CPU服务软中断所耗费的时间总额
- st Steal time 虚拟机被hypervisor偷去的CPU时间(如果当前处于一个hypervisor下的vm,实际上hypervisor也是要消耗一部分CPU处理时间的)
第4行
- 物理内存总量
- 使用的物理内存总量
- 空闲内存总量
- 用作内核缓存的内存量
第5行
- 交换区总量
- 使用的交换区总量
- 空间交换区总量
- 缓冲交换区总量
进程信息
在top命令中按f按可以查看显示的列信息,按对应字母来开启/关闭列,大写字母表示开启,小写字母表示关闭。带*号的是默认列。
A: PID = (Process Id) 进程Id;
E: USER = (User Name) 进程所有者的用户名;
H: PR = (Priority) 优先级
I: NI = (Nice value) nice值。负值表示高优先级,正值表示低优先级
O: VIRT = (Virtual Image (kb)) 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES
Q: RES = (Resident size (kb)) 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA
T: SHR = (Shared Mem size (kb)) 共享内存大小,单位kb
W: S = (Process Status) 进程状态。D=不可中断的睡眠状态,R=运行,S=睡眠,T=跟踪/停止,Z=僵尸进程
K: %CPU = (CPU usage) 上次更新到现在的CPU时间占用百分比
N: %MEM = (Memory usage (RES)) 进程使用的物理内存百分比
M: TIME+ = (CPU Time, hundredths) 进程使用的CPU时间总计,单位1/100秒
b: PPID = (Parent Process Pid) 父进程Id
c: RUSER = (Real user name)
d: UID = (User Id) 进程所有者的用户id
f: GROUP = (Group Name) 进程所有者的组名
g: TTY = (Controlling Tty) 启动进程的终端名。不是从终端启动的进程则显示为 ?
j: P = (Last used cpu (SMP)) 最后使用的CPU,仅在多CPU环境下有意义
p: SWAP = (Swapped size (kb)) 进程使用的虚拟内存中,被换出的大小,单位kb
l: TIME = (CPU Time) 进程使用的CPU时间总计,单位秒
r: CODE = (Code size (kb)) 可执行代码占用的物理内存大小,单位kb
s: DATA = (Data+Stack size (kb)) 可执行代码以外的部分(数据段+栈)占用的物理内存大小,单位kb
u: nFLT = (Page Fault count) 页面错误次数
v: nDRT = (Dirty Pages count) 最后一次写入到现在,被修改过的页面数
y: WCHAN = (Sleeping in Function) 若该进程在睡眠,则显示睡眠中的系统函数名
z: Flags = (Task Flags <sched.h>) 任务标志,参考 sched.h
X: COMMAND = (Command name/line) 命令名/命令行参考资料
- 当前系统时间
-
云服务器安全相关配置
我所租用的云服务器操作系统为CentOS Linux,在使用云服务器的过程中为了保证服务器的安全,进行了一些简单的配置,这样能够增加服务器被破解的难度。
-
C风格和C++风格的文件操作库函数总结
由于C++是兼容C的,所以使用C++进行文件操作时,会发现有两套库函数可以使用,它们分别是C风格的和C++风格的。
C风格的文件操作函数
打开文件,关闭文件
#include <stdio.h> FILE* fopen(const char* path, const char* mode); int fclose(FILE* fp);
读取文件,写入文件
#include <stdio.h> size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream); size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream);
关于fread中两个表示大小的参数,在这个 stackoverflow 问题中得到了比较好的解答。
改变文件指示器的位置
#include <stdio.h> int fseek(FILE* stream, long offset, int whence); long ftell(FILE* stream); void rewind(FILE* stream);
C++风格的文件操作函数
C++将文件操作函数封装为
fstream
操作类,一个文件实际上就会对应一个fstream
对象,一下函数都是fstream
的成员函数。
文件打开和关闭void open(const char* filename, ios_base::openmode mode = ios_base::in|ios_base::out); void open(const std::string& filename, ios_base::openmode mode = ios_base::in|ios_base::out); void close();
对于文件的读写可以才采用流运算符重载的方法完成。
friend fstream& operator << (fstream& ofs, const CRecord& objRecord); friend fstream& operator >> (fstream& ifs, CRecord& objRecord);
题外话
在liunx下,可以使用wc命令查看文件中的相关信息。wc命令会打印出该文件的行数、单词数、字符数。
-
字节序与union类型
字节序是什么
字节序是指在数据传输时,高位字节存储在内存中的较高位地址还是较低位地址。字节序分为大端字节序和小端字节序,两种字节序不能混用。
- 大端字节序 :高位字节保存在较低位地址的内存中
- 小端字节序 :高位字节保存在较高位地址的内存中
以
int n = 0x12345678;
为例,大端字节序内存中的存储的顺序为0x12 0x34 0x56 0x78
,小端字节序内存中的存储顺序为0x78 0x56 0x34 0x12
。很明显可以看出大端字节序更符合人类的阅读习惯。- 常见的大端系统CPU:IBM z/Atchitecture
- 常见的小端系统CPU:intel x86
网络字节序使用的大端字节序,常用网络协议如IPv4、IPv6、TCP和UDP协议都是使用大端字节序进行数据传输的。
提供字节序转换功能的函数接口
在网络通信接口中提供了转换主机字节序和网络字节序之间的函数接口,如下所示。
uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
有些时候,我们需要手动转换内存字节流,可以使用以下接口帮助实现指定字节序的转换。
小端字节序的函数实现如下:
// small endian inline std::uint16_t SE_GetStreamU16(const void* p) { const std::uint8_t* pTemp = static_cast<const std::uint8_t*>(p); return std::uint16_t(pTemp[0] | (pTemp[1] << 8)); } inline std::uint32_t SE_GetStreamU32(const void* p) { const std::uint8_t* pTemp = static_cast<const std::uint8_t*>(p); return std::uint32_t(pTemp[0] | (pTemp[1] << 8) | (pTemp[2] << 16) | (pTemp[3] << 24)); } inline void SE_SetStreamU16(void* p, std::uint16_t n) { std::uint8_t* pTemp = static_cast<std::uint8_t*>(p); pTemp[0] = std::uint8_t(n); pTemp[1] = std::uint8_t(n >> 8); } inline void SE_SetStreamU32(void* p, std::uint32_t n) { std::uint8_t* pTemp = static_cast<std::uint8_t*>(p); pTemp[0] = std::uint8_t(n); pTemp[1] = std::uint8_t(n >> 8); pTemp[2] = std::uint8_t(n >> 16); pTemp[3] = std::uint8_t(n >> 24); }
大端字节序的函数实现如下:
// big endian inline std::uint16_t BE_GetStreamU16(const void* p) { const std::uint8_t* pTemp = static_cast<const std::uint8_t*>(p); return std::uint16_t(pTemp[1] | pTemp[0] << 8); } inline std::uint32_t BE_GetStreamU32(const void* p) { const std::uint8_t* pTemp = static_cast<const std::uint8_t*>(p); return std::uint32_t(pTemp[3] | (pTemp[2] << 8) | (pTemp[1] << 16) | (pTemp[0] << 24)); } inline void BE_SetStreamU16(void* p, std::uint16_t n) { std::uint8_t* pTemp = static_cast<std::uint8_t*>(p); pTemp[0] = std::uint8_t(n >> 8); pTemp[1] = std::uint8_t(n); } inline void BE_SetStreamU32(void* p, std::uint32_t n) { std::uint8_t* pTemp = static_cast<std::uint8_t*>(p); pTemp[0] = std::uint8_t(n >> 24); pTemp[1] = std::uint8_t(n >> 16); pTemp[2] = std::uint8_t(n >> 8); pTemp[3] = std::uint8_t(n); }
查看当前系统的字节序
通过
lscpu
命令,能得到cpu的大端和小端信息。lscpu | grep -i endian
通过常用shell命令,在大端系统中会输出0,在小端系统中会输出1。
echo -n I | od -to2 | head -n1 | cut -f2 -d" " | cut -c6
借助python进行判断。
python -c "import sys; print(sys.byteorder)" # or python -c "import sys;sys.exit(0 if sys.byteorder=='big' else 1)"
借助linux的ELF进行判断,查看第6个字节,小端系统中为1,大端系统中为2。
xxd -c 1 -l 6 /bin/bash # or hexdump -n 6 -C /bin/bash
可以使用union类型验证字节序,以下示例代码在不同的字节序下会有不同的输出。
#include <stdio.h> #include <inttypes.h> typedef union un { int32_t x; char ch[4]; } un; int main() { un u; u.ch[0] = 1; u.ch[1] = 2; u.ch[2] = 3; u.ch[3] = 4; printf("u.x=0x%x\n", u.x); return 0; }
- 大端字节序 :高位字节保存在较低位地址的内存中
-
在Python日志中输出文件名和函数名
Python内置了日志模块,在默认情况下输出的日志是不带文件名和函数名的,这样在排查问题时,遇到相似的日志就变得容易混淆,可以通过设置将输出的日志中带有文件名和函数名。参考了stackoverflow的回答,详细代码如下。
import logging log = logging.getLogger('root') LOG_FORMAT = "%(filename)s:%(lineno)s %(funcName)s() %(message)s" logging.basicConfig(format=LOG_FORMAT) log.setLevel(logging.DEBUG)
参考资料
stackoverflow -
Linux常用命令行指令 - xargs
基础用法
xargs
指令默认情况下从stdin
中读取信息,按照空格分或换行区分开,并且执行输入的命令(默认的命令是/bin/echo
)。命令可以通过xargs
的参数指定执行一次或多次,命令的执行参数为自身初始参数追加上由stdin
读到的内容,命令在执行时会忽略空行。xargs reads items from the standard input, delimited by blanks (which can be protected with double or single quotes or a backslash) or newlines, and executes the command (default is /bin/echo) one or more times with any initial-arguments followed by items read from standard input. Blank lines on the standard input are ignored.
在命令行输入
xargs
命令,输入 “a b c d” 后按回车,再按ctrl+d
完成本次输入,则xargs
的默认echo
命令会将接收到的字符串打印出来,效果如下所示。# xargs a b c d a b c d
xargs
单独使用时功能比较单一,但是它与其他指令一起使用十分强大。xargs
与find
组合使用常见的使用方法是使用
find
指令找到指定的文件,组合以xargs
指令实现对指定文件的操作。find . -name "demo_" | xargs file -i
由于
xargs
是使用空格作为各个参数的分隔符,如果遇到的文件名中含有空格,则并不能按照预期处理。这时,可以使用find
指令的-print0
将含有空格的文件名转化为字符串,再结合xargs
的-0
参数就能完成文件名中含有空格的处理,改进后的命令如下。find . -name "demo_" -print0 | xargs -0 file -i
xargs
与grep
组合使用常用命令如下,能够实现对指定规则文件的搜索匹配。
find . -name "*.log" | xargs grep error
使用
-n
参数让xargs
的命令重复执行如果想用
find
指令完成多个匹配规则的搜索,则可以使用-n
参数将xargs
的内容分割成多个。
下面的命令会执行3次find
命令,xargs
会将接收的到的内容按照单词数为“1”分割,分3次传送给find
指令。echo "*.c" "*.cpp" "*.h" | xargs -n 1 find . -name
使用
-t
参数让xargs
打印将要执行的命令让
xargs
将指令的命令打印出来主要是方便查看命令执行情况,也方便在出现错误时进行调试。find . -name *.log | xargs -t rm -f
使用
-p
参数让xargs
在执行命令前询问用户增加
-p
参数后,xargs
在每次执行命令前会将指令打印出来并询问用户,只有用户输入 “y” 时才会真正执行该条命令。echo "*.c" "*.cpp" "*.h" | xargs -n 1 -p find . -name
使用
-I
参数让xargs
进行参数替换xargs
默认会将读取到的内容追加在指令命令后面作为参数,使用-I
参数能够控制命令参数的组合方式。
以下命令会将当前目录中的可执行文件移动到bin文件夹下。find . -executable -type f -print0 | xargs -0 -I {} mv {} ./bin
-
Linux常用命令行指令 - ipcs
IPC
是inter process communication
的缩写,这项技术能够让进程间相互通信。
Q:每个进程都有自己的地址空间和独立的用户空间,那么进程间是如何通信的呢?
A:内核,也就是操作系统的心脏,它能够访问整个操作系统的内存。我们可以要求内核分配一块用于进程间交互的空间。几种进程间通信的方法
进程间通信的方法有很多,有些支持同机器上进程的信息交互,有些支持跨机器的进程交互。
-
管道 : pipes,管道提供了进程间交换信息的方法。
-
共享内存 : shared memory,一个进程创建一块其他进程能够访问的内存空间,多个进程可以通过共享内存进行数据交换。
-
消息队列 : message queue,消息队列是一个固定结构、有序的内存段,多个进程可以存放和取回数据。
-
信号量 : semaphores,信号量提供了多进程访问同一资源的同步机制,信号量不负责传递数据,它协调对共享资源的访问。
常用ipcs指令
列出所有的IPC设备
ipcs -a
列出所有的消息队列
ipcs -q
列出所有的信号量
ipcs -s
列出所有的共享内存
ipcs -m
获取与IPC设备信息
ipcs -q -i msq_id
列出IPC设备的限制
ipcs -l
列出IPC设备的创建者和拥有者
ipcs -m -c
列出最近使用IPC设备的进程id
ipcs -m -p
列出IPC设备的最后访问时间
ipcs -s -t
列出IPC设备的当前使用状态
ipcs -u
-
管道 : pipes,管道提供了进程间交换信息的方法。
-
使用gdb调试多线程程序
查看当前线程信息
将进程中的各个线程信息显示出来
(gdb) info threads
切换到指定进程
(gdb) thread tid
向指定的线程发送自定的指令
(gdb) thread apply tid/all args
常用的指定是查看所有线程的调用堆栈
thread apply all bt
,这个指令与pstack
命令有些相似。gdb默认会自动捕捉新产生线程
会在产生一个新的线程时会显示出LWP的字样提示用户,LWP = light weight process
可以设置gdb是否提示线程相关的事件(gdb) set print thread-events on/off (gdb) show print thread-events
为指定的线程设置断点
含有多线程的程序,可以为单独的线程设置断点
(gdb) break linespec thread tid
任何时候当你的程序在GDB模式下停止的时候,包括当前调试线程的所有线程都会停下来,不会对继续对当前进程造成更改。这时你可以在线程间进行切换,查看整个进程的执行状况。
Whenever your program stops under GDB for any reason, all threads of execution stop, not just the current thread. This allows you to examine the overall state of the program, including switching between threads, without worrying that things may change underfoot.
防止gdb自动切换线程
在调试gdb程序时,在单步执行时,会出现线程间跳转切换,这样对跟踪代码执行状态十分不方便。
可以通过设置scheduler-locking
让gdb在所调试的线程中运行,防止线程的自动切换。(gdb) set scheduler-locking step
可以执行以下命令查看当前
scheduler-locking
的设置(gdb) show scheduler-locking
scheduler-locking
有三种模式-
off
任何线程在任何时候都能执行
-
on
只有当前线程能够执行
-
step
为单步执行优化的模式,比较适合一般的调试
Set the scheduler locking mode. If it is off, then there is no locking and any thread may run at any time. If on, then only the current thread may run when the inferior is resumed. The step mode optimizes for single-stepping. It stops other threads from "seizing the prompt" by preempting the current thread while you are stepping. Other threads will only rarely (or never) get a chance to run when you step. They are more likely to run when you `next' over a function call, and they are completely free to run when you use commands like `continue', `until', or `finish'. However, unless another thread hits a breakpoint during its timeslice, they will never steal the GDB prompt away from the thread that you are debugging.
-
-
在gdb调试时忽略系统信号(signal)
在gdb调试程序时,默认情况下gdb在收到信号时会中断程序的运行,并将收到的信号显示出来。这时,可以选择输入
c
(continue)让程序继续运行。如果程序会重复收到这信号,会非常影响调试效率。可以通过配置忽略指定的系统信号。查看当前系统信号的处理信息的指令如下。
(gdb) info signal
以调试网络程序为例,进程会经常收到
SIGPIPE
消息,对于网络进程的SIGPIPE
消息在程序中会由自身处理,可以使用以下指令让gdb不再提示SIGPIPE
信号。(gdb) handle SIGPIPE nostop noprint
其中,
nostop
表示在收到信号时不再中断程序的运行,noprint
表示在收到信号时不再将收到的信号打印到gdb调试界面,这两个参数可以分开使用。参考资料: gnu gdb manual
(全文完)
-
C++11新特性:基于范围的for循环
在C++11中,介绍了一种新的for循环写法,基于范围的for循环。
与旧的for循环类似,也是用来对遍历容器中的所有元素,新的写法更加简洁方便。可以使用auto自动适配容器中的内容。为了遍历容器时提高效率,在访问容器内部时可以声明为元素的引用,进而避免不必要的对象拷贝。示例代码如下(代码节选自 cppreferance)std::vector<int> v = {0, 1, 2, 3, 4, 5}; for (const int& i : v) // access by const reference { std::cout << i << ' '; } std::cout << '\n'; for (auto i : v) // access by value, the type of i is int { std::cout << i << ' '; } std::cout << '\n'; for (int n : {0, 1, 2, 3, 4, 5}) // the initializer may be a braced-init-list { std::cout << n << ' '; } std::cout << '\n'; int a[] = {0, 1, 2, 3, 4, 5}; for (int n : a) // the initializer may be an array { std::cout << n << ' '; } std::cout << '\n'; for (int n : a) { std::cout << 1 << ' '; // the loop variable need not be used } std::cout << '\n';
注意
std::map
的范围for循环遍历的写法std::map
中存放的是key-value pair
键值对,所以在遍历的时候每次拿到的是一个pair
,在访问其中的元素时,需要注意书写方法。示例代码如下。std::map<int, std::string> mapDemo; for (auto& kvp : mapDemo) { std::cout << kvp.first; std::cout << " - "; std::cout << kvp.second; std::cout << std::endl; }