strace浅谈

很多公司,面试linux运维喜欢问bash编程,实际上,bash编程的很多语法细节和参数是很容易忘记的,比如将本机的8888端口转发至192.168.42.122的8080,端口转发的命令为:ssh -C -f -N -g -L 8888:192.168.42.122:8080 [email protected],这一大堆参数,如果不是前阵子刚写过,多半就忘记了,问这些细节其实意义不大。那么,问什么能体现一个中高级linux运维的基本功呢?个人觉得strace是其中之一,分析strace可以很好地体现一个工程师对linux系统调用的理解水平。如果一个运维,经常与研发的linux c程序员沟通学习,对系统调用的理解水平可以获得很大提高。


OK,废话说完。要理解strace,需要哪些基础知识呢?

strace的用法就不说了,看man,很简单。关键是要理解一些系统调用函数,个人觉得下面这些函数是必须了解的:

1、read和write

ssize_t read(int fd,void *buf,size_t nbyte)
ssize_t write(int fd, const void*buf,size_t nbytes);

read函数从fd中读取内容,当读取成功时,返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起 的,如果是ECONNREST表示网络连接出了问题。

例子:
open("/opt/app/oracle/product/11.2.0/db_home_1/lib/libclntsh.so.11.1", O_RDONLY) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0PYA\0\0\0\0\0"..., 832) = 832
这里先以只读模式打开文件libclntsh.so.11.1,然后读了832个字节。


write函数将buf中的nbytes字节内容写入文件描述符fd,成功时返回写的字节数,失败时返回-1,并设置errno变量。

例子:
write(1, "ERROR:\n", 7)                 = 7
write(1, "ORA-12547: TNS:lost contact\n", 28) = 28
这里是向标准输出stdout(fd=1)输出东西,返回的是字节数。


除了写普通文件,还能写socket文件。在网络程序中,当我们向socket写时有两可能:

1)write的返回值大于0,表示写了部分或者是全部的数据。这样我们用一个while循环来不停的写入,但是循环过程中的buf参数和nbyte参数得由我们来更新。
也就是说,网络写函数是不负责将全部数据写完之后再返回的。

2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理,如果错误为EINTR表示在写的时候出现了中断错误。
如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)


2、stat、fstat、lstat

int fstat(int filedes, struct stat *buf);
int stat(const char *path, struct stat *buf);
int lstat(const char *path, struct stat *buf);

这三个函数的目的是获取文件的某些信息,用来将参数fildes所指的文件属性,复制到参数buf所指的结构中(struct stat)。第一个函数用fd作为参数,第二和第三个函数用文件的绝对路径作为参数。


看个例子:

open("/opt/app/oracle/product/11.2.0/db_home_1/lib/libsqlplus.so", O_RDONLY) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0p\366\1\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=1470768, ...}) = 0


这里打开文件libsqlplus.so,读取了832个字节,然后用fstat获取了文件的类型、权限和大小。


3、lseek

目的:lseek函数可以改变文件的cfo

所有打开的文件都有一个当前文件偏移量(current file offset),以下简称为 cfo。
cfo 通常是一个非负整数,用于表明文件开始处到文件当前位置的字节数。读写操作通常开始于 cfo,
并且使 cfo增大,增量为读写的字节数。文件被打开时,cfo 会被初始化为 0,除非使用了O_APPEND


4、fcntl

fcntl()函数用于对文件进行某些控制

用法:
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
 
参数:   
fd:文件描述符。
cmd:操作命令。
arg:供命令使用的参数。
lock:同上。


例子:

看下面的strace片段:
open("/opt/app/oracle/product/11.2.0/db_home_1/sqlplus/mesg/sp1us.msb", O_RDONLY) = 3
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0   // 这里表示当程序执行exec函数时本fd将被系统自动关闭,表示不和子进程共享该文件
lseek(3, 0, SEEK_SET)                   = 0

注:close-on-exec是什么意思呢?
当close-on-exec标志置为1时,即开启此标志。那么当子进程调用exec函数之前,系统就已经让子进程将此文件描述符关闭了。
当close-on-exec标志置为0时,即关闭了此标志。那么当子进程调用exec函数,子进程将不会关闭该文件描述符。此时,父子进程将共享该文件。


5、mmap

mmap函数用来将文件映射进虚拟内存,这样就可以直接对该内存进行操作,从而省去IO操作

mmap有几个参数:
prot,映射区的保护方式,可以是下面三种:
PROT_EXEC: 映射区可被执行
PROT_READ: 映射区可被读取
PROT_WRITE: 映射区可被写入

flags,映射区的特性
MAP_SHARED:写入映射区的数据会复制回文件, 且允许其他映射该文件的进程共享。
MAP_PRIVATE:对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件

有个跟mmap相关的函数:mprotect,用于设置内存访问权限。

mmap 的第三个参数指定对内存区域的保护,由标记读、写、执行权限的 PROT_READ、PROT_WRITE 和 PROT_EXEC 按位与操作获得,或者是限制没有访问权限的 PROT_NONE。
如果程序尝试在不允许这些权限的本地内存上操作,它将被 SIGSEGV 信号(Segmentation fault,段错误)终止。

在内存映射完成后,这些权限仍可以被 mprotect 系统调用所修改。mprotect 的参数分别为内存区间的地址,区间的大小,新的保护标志设置。所指定的内存区间必须包含整个页:区间地址必须和整个

系统页大小对齐,而区间长度必须是页大小的整数倍。这些页的保护标记被这里指定的新保护模式替换。

比如,首先用mmap映射一个文件到内存:
int fd = open ("/dev/zero",O_RDONLY);
char* memory = mmap (NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
close (fd);

可以看到,映射模式为可读和可写模式。

然后,可以用mprotect进行修改,将其变为只读:
mprotect (memory, page_size, PROT_READ);

还有一个相关函数munmap,从名字也可以猜测出来,其目的是:释放内存映射。
munmap(0x2b2814d60000, 120737)          = 0
对应前面的:mmap(NULL, 120737, PROT_READ, MAP_PRIVATE, 3, 0) = 0x2b2814d60000


6、socket、bind、poll


socket:建立套接字,成功时返回文件描述符,失败时返回-1
bind:当创建了一个socket后,接下来的工作,就是调用bind指定一些socket参数,如:本地址、协议端口号等

poll:判断fds中的文件是否可读,如果可读则会立即返回,返回的值就是可读fd的数量,
如果不可读,那么进程就会休眠timeout这么长的时间,然后再来判断是否有文件可读,
如果有,返回fd的数量,如果没有,则返回0


socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP) = 6
bind(6, {sa_family=AF_INET6, sin6_port=htons(0), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0

7、send和recv,sendto和recvmsg

有了send和recv,为何还需要sendto和recvmsg?(前面讲的read和write,也可以用来读写socket,实现基本的socket通信)对于发送消息,send只可用于基于连接的socket,send和write的唯一区别是标志的存在,当标志为0时,send等同于write
sendto和sendmsg既可用于有连接socket,也可用于无连接socket。除非socket设置为非阻塞模式,否则调用将会阻塞直到所有数据发送完毕。

ssize_t send(int sock, const void *buf, size_t len, int flags);
ssize_t sendto(int sock, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
ssize_t sendmsg(int sock, const struct msghdr *msg, int flags);

参数:
sock:索引将要从其发送数据的套接字。
buf:指向将要发送数据的缓冲区。
len:以上缓冲区的长度。
flags:是以下零个或者多个标志的组合体,可通过or操作连在一起:
    MSG_DONTROUTE:不要使用网关来发送封包,这个标志告诉IP层,目的主机在本地网络上,没有必要查找表,这个标志一般用在网络诊断和路由程序里面
    MSG_DONTWAIT:操作不会被阻塞。
    MSG_EOR:终止一个记录。
    MSG_MORE:调用者有更多的数据需要发送。
    MSG_NOSIGNAL:当另一端终止连接时,请求在基于流的错误套接字上不要发送SIGPIPE信号。
    MSG_OOB:发送out-of-band数据(需要优先处理的数据),同时现行协议必须支持此种操作。


8、futex

Futex(Fast Userspace Mutex)快速用户态互斥体,它是一种由用户态和内核态共同完成的同步机制
在linux下,信号量和线程互斥锁的实现都是通过futex系统调用。(与之对等的还有pthreads mutexes)

创建时是在用户空间通过mmap申请一片共享内存以便多进程间共同访问此futex,用户程序首先访问用户
态的futex,只在判断出存在冲突(如一个进程已经拥有此futex,另一个进程申请访问,此时便存在一个冲突)时才进入内核态进行仲裁同步。

用户空间的访问和冲突判断由glibc库完成,冲突仲裁由内核的futex模块完成

内核仲裁:将用户态的锁置上等待标志表明有锁的等待者存在,并调用schedule()将锁申请者挂起;当锁的拥有
者释放锁(由glibc库完成)时,检查发现该锁有等待者就进入内核将等待者唤醒。



例子:
futex(0x35a1b54a08, FUTEX_WAKE_PRIVATE, 2147483647) = 0


9、signal相关函数
信号是与进程相联系的。也就是说,一个进程可以决定对哪些信号进行什么样的处理。例如,一个
进程可以忽略某些信号而只处理其它一些信号;另外,一个进程还可以选择如何处理信号。
因此,首先要建立信号和进程的对应关系,这就是信号的安装登记。

Linux主要有两个函数实现信号的安装登记:signal和sigaction。其中signal在系统调用的基础上实现,是库函数。
它只有两个参数,不支持信号传递信息,主要是用于前32个非实时信号的安装;而sigaction是较新的函数(由两个
系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与sigqueue系统
调用配合使用。当然,sigaction同样支持非实时信号的安装,sigaction优于signal主要体现在支持信号带有参数


sigaction用于改变进程在接收到特定信号后的行为。
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

第一个参数为信号的值,可以为除SIGKILL和SIGSTOP之外的任何一个特定的有效信号值。第二个参数是一个指向sigaction
结构实例的一个指针,在实例中,指定了对特定信号的处理。第三个参数用来保存原来对相应信号的处理,可以为null
如果把第二个和第三个参数都设为null,函数可用来检查信号的有效性。

sigaction结构体如下:
struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};

例子:
rt_sigaction(SIGRTMIN, {0x35a24053c0, [], SA_RESTORER|SA_SIGINFO, 0x35a240ebe0}, NULL, 8) = 0
rt_sigaction(SIGRT_1, {0x35a24052f0, [], SA_RESTORER|SA_RESTART|SA_SIGINFO, 0x35a240ebe0}, NULL, 8) = 0


注:以上对函数的说明大部分来自互联网上的零散资料,穿插的一些例子是笔者自己补充的。

你可能感兴趣的:(linux,strace)