目录
前记
linux系统编程中一些杂记。重新整理下。关于这一块,当时是有看过一本《程序的自我修养》,对编译原理以及堆栈等讲的还挺详细了。这会也忘得差不多了,就目前根据是实际使用过程中,把有用的都简单整理下,方便以后直接能记起来。
一、程序的编译
进程就是执行中的程序,程序要执行前需加载到内存中. 一个程序可以执行多次,也就可以多个进程。
(1)编译的过程
编译过程: 预处理 ----> 编译 ----> 汇编 ----> 链接
1)预处理(Pre-processing)
在该阶段,编译器将C源代码中的包含的头文件如stdio.h添加进来
参数:”-E”
用法:gcc -E hello.c -o hello.i
作用:将hello.c预处理输出hello.i文件。
2)编译(Compiling)
第二步进行的是编译阶段,在这个阶段中,gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。
参数:”-S”
用法:gcc –S hello.i –o hello.s
作用:将预处理输出文件hello.i汇编成hello.s文件。
3)汇编(Assembling)
汇编阶段是把编译阶段生成的”.s”文件转成二进制目标代码“.o”文件
参数:“-c”
用法:gcc –c hello.s –o hello.o
作用:将汇编输出文件hello.s编译输出hello.o文件。
4)链接(Link)
在成功编译之后,就进入了链接阶段。
用法:gcc hello.o –o hello
作用:将编译输出文件hello.o链接成最终可执行文件hello。
一般我们写Makefile 就是安装上面的步骤编译.o 再链接成二进制文件,最后还要strip去掉符号,减小bin文件大小
(2)程序运行原理
一个程序镜像里分有: xx段(section)
分有很多个段, 通常我们要注意的有: 代码段(.text), 数据段(.data), 只读数据段(.rodata)
未初始化的数据段(.bss)
反汇编: objdump -D 可执行文件
段: 其实就是表示一个文件内的区域(从文件的第几个字节 ---到文件内的第几个字节结束)
代码段:用于存放需要执行的代码(编译完后是机器码,反汇编时,编译器会把机器码转为相应的汇编语句)
数据段: 存放初始化过全局变量, 初始化过的静态变量。 也就是这些变量在编译时编译器分配在这个区域里的。
未初始化数据段: 存放没初始化的全局变量, 没初始化的静态变量。 这些变量在编译时,不会分配具体的空间,只是在bss段里记录所需的空间。 在程序执行时,系统才会分配具体的空间。
只读数据段: 存放字符串常量, 只读全局变量(const). 这些变量在编译时,编译器会在这个区域里分配具体的空间
char *p = "hello world"; //字符串"hello world"的内容存入只读数据段
//至于指针p分配在哪个段与它的作用域有关
(3)程序空间
局部变量的空间在代码执行时才会在栈里分配出来
栈(stack): 系统里用于分配局部变量的空间区域, 分配出来的空间地址是从高往低分配的.
系统里只有一个栈. 一个函数执行时,分从栈里分配局部变量的空间, 函数执行结束时会回收栈里分配的空间。回收的空间会重用的。
在系统里每个进程在栈里所使用的大小空间有限制(cenos7上不超过8M).
避免爆栈, 尽量使用动态分配空间
堆(heap) : 系统里用于动态分配空间的区域,分配出来的空间地址是从低往高分配的。
系统里只有一个堆。 每个进程在堆里使用的空间是没有限制大小, 只要系统还有可用内存即可申请成功。
动态分配出来的空间需要调用释放函数回收。
malloc() --- free()
(4)进程内部的资源
文件描述符是进程内部的资源。不同的进程中有相同号的文件描述符,号相同并不代表所操作的对象也相同.
一个进程里默认只能有1024个文件描述符. (0-2) (3~1023) 。0,1,2是标准输出,标准输入和标准错误
如文件描述符close后,可重用。
strace 可执行文件 //可查看此执行文件的系统调用过程
valgrind 可执行文件 //查看有没有内存泄露
nm 可执行文件 //可查看一个程序里的全局变量,函数编译时所分配的地址
file 可执行文件 //可查看程序的编译版本,架构等
readelf 程序 //可查看程序所需的C库版本等文件头信息
ldd 程序 //可查看程序所需的动态库
strip 程序 //把程序中的符号信息去掉,无法用nm等命令查看程序.
(5)进程的状态
///进程的状态(通过man ps)
D Uninterruptible sleep (usually IO) 不间断睡眠(通常为IO)
R Running or runnable (on run queue) 正在运行或可运行(运行队列上)
S Interruptible sleep (waiting for an event to 可中断睡眠(等待事件完成)
complete)
T Stopped, either by a job control signal or because 因为作业控制信号或正在追踪。
it is being traced.
X dead (should never be seen) 死亡(永远不会被看到)
Z Defunct ("zombie") process, terminated but not
reaped by its parent. 失效(“僵尸”)进程,已终止但未终止被它的父母收获。
< high-priority (not nice to other users) 高优先级(对其他用户不好)
N low-priority (nice to other users) 低优先级(对其他用户友好)
L has pages locked into memory (for real-time and
custom IO) 将页面锁定在内存中(用于实时和自定义IO)
s is a session leader
l is multi-threaded (using CLONE_THREAD, like NPTL
pthreads do) 是多线程的(使用克隆线程,比如NPTL)
+ is in the foreground process group 在前台进程组中
二、进程
每个进程都会拥有一个唯一的id号, 也就是PID
父子进程:
假设A进程调用了B程序, 产生了B进程,
那么对于B进程来说A进程就是它的父进程, 对A进程来说B进程就是它的子进程
1. getpid(), 得到当前进程的pid号
getppid(), 得到当前进程的父进程pid号
2. nice(val), 设置当前进程的优先级别(-20 -- 19), 默认级别为0, -20为最高, 19最低
setprioity(which, who, val);
当which为:PRIO_PROCESS, who为要设优先级别的进程号, val为优先级别数
当为: PRIO_PGRP, who为要设的进程组的id号
当为: PRIO_USER, who为要设的user id
3. fork() 创建一个子进程
等于把父进程完整的复制一份副本,而这个副本就成为了子进程, 包括父进程里的变量,函数,文件描述符等,在子进程里同样具有
子进程创建出来后,它从fork()函数后开始运行的,而不是从main函数,在子进程里得到fork()的返回值是0, 父进程得到子进程的pid号
4. 僵尸进程: 当子进程已退出, 父进程还在运行时,则子进程变成了僵尸进程,直到被父进程收尸或父进程退出
pid_t wait(int *status); 用于收尸
调用此函数时会堵塞当前进程的运行,直到有子进程结束为止,函数返回值为结束的子进程的pid号
WIFEXITED(status)用于检查是否正常退出 exit, _exit,return等主动退出的都是正常退出,如被kill则是非正常退出
WEXITSTATUS(status)得到子进程的返回值
如要等待指定的子进程退出时可用waitpid函数
5.exit // return // _exit都是正常结束进程
exit//return结束时会关闭打开的文件描述符,处理缓冲区的数据
_exit不会处理缓冲区的数据
6. int atexit(void (*func)(void));
注册一个函数在进程正常退出时自动运行,可用于处理退出前的工作
7. 孤儿进程: 当父进程退出后,而子进程还在运行,则成为孤儿进程,
孤儿进程会被init(systemd)进程(pid=1)接管,所以孤儿进程的ppid=1,当孤儿进程结束运行时,init进程会收它的尸
8. vfork() 同fork()函数功能一样, 但在复制父进程时,不会完整的复制. 适用于创建一个子进程后,马上使用exec*函数调用其它程序.
vfork会堵塞会父进程的执行, 直到子进程执行完毕.
9. demon守护进程, 也就是在后台运行的进程
创建步骤: 1.先创建出一个孤儿进程
2. setsid(), 让此进程成为进程组的组长并关闭终端
3.关闭标准输出,标准错误输出
4. 把当前的工作目录改变到"/"
10. exec调用程序
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,
..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
以上函数execve才是真正的系统调用函数
注意最后一个参数必须是NULL
"l"字母表示参数是单个的参数
"v"字母表示参数是数组形式的
有"p"字母的表示要调用的程序在PATH环境变量里查找,
没有"p"字母的要给绝对路径
有"e"字母的表示可指定什么环境变量给要调用的程序,没有"e"字母的表示让要调要的程序继承当前进程的环境变量
11. 环境变量的值
在shell上env可查看当前系统的环境变量
子进程默认会继承父进程里的环境变量,在子进程里改变环境变量不会影响父进程
extern char **environ; 获取系统的环境变量
getenv(char *name) 获取指定名字的环境变量值, 找不到返回NULL
setenv(char *name, char *val, int overwrite) 设置环境变量的值
如果指定的变量名已存在而且overwrite不为0,则改定变量的值 如果指定的变量名已存在而且overwrite为0,则不改定变量的值
三、线程
一个进程里可以有多个线程,也就是可以多个同时执行的代码线路分支。
系统的进程调度的最小单元为线程, 进程作为资源最小分配单元
每个进程都会有一个主线程(从main函数开始执行的线程), 并且可以有多个非主线程,当主线程退出时,则整个进程和所有属于此进程的线程都会结束运行。一个线程只能属于一个进程。
一个进程里的所有线程可以共用进程的数据段,代码段,只读数据段。。。。
一个进程里的线程不用线程间的通信。
线程库: gcc -lpthread 编译的时候需要指定带此库。
int pthread_join(pthread_t thread, void **value_ptr);
//调用此函数的当前线程会堵塞直到指定的ID为thread的线程结束运行为止, 并把返回值保存到value_ptr里. 并清理thread线程所使用的资源
int pthread_detach(pthread_t thread); //分离指定的线程,当thread线程结束运行时,会自动清理所使用的资源
int pthread_cancel(pthread_t thread); //取消线程的执行.
对于所创建出来的线程,pthread_join或者pthread_detach必须用一个。主线程要么事分离线程,要么是等待子线程结束
四、进程与线程的区别
1.多进程和多线程的区别,前者开销大,后者开销较小。确实,这就是最基本的区别。
2.线程函数的可重入性:
要确保函数线程安全,主要需要考虑的是线程之间的共享变量。属于同一进程的不同线程会共享进程内存空间中的全局区和堆,而私有的线程空间则主要包括栈和寄存器。因此,对于同一进程的不同线程来说,每个线程的局部变量都是私有的,而全局变量、局部静态变量、分配于堆的变量都是共享的。在对这些共享变量进行访问时,如果要保证线程安全,则必须通过加锁的方式
可重入的判断条件:
要确保函数可重入,需满足一下几个条件:
1、不在函数内部使用静态或全局数据
2、不返回静态或全局数据,所有数据都由函数的调用者提供。
3、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
4、不调用不可重入函数。
如果我们的线程函数不是线程安全的,那在多线程调用的情况下,可能导致的后果是显而易见的——共享变量的值由于不同线程的访问,可能发生不可预料的变化,进而导致程序的错误,甚至崩溃。
多数的多线程都是在同一个进程下的,它们共享该进程的全局变量,我们可以通过全局变量来实现线程间通信。如果是不同的进程下的2个线程间通信,直接参考进程间通信
多进程要并发协调工作,进程间的同步,通信是在所难免的。比如:在系统做配置更改时,需要能够通知到到对应的进程 重新加载新的参数配置运行。 就可以使用一个专门管理进程的控制程序,类似于openwrt中的proced等机制。
五、进程间通信:
在linux下有多种进程间通信的方法:无名管道、命名管道、内存映射、消
息队列、共享内存、信号量、信号、文件,套接字及环境变量等等。
无名管道(pipe)
命名管道(fifo)
内存映射(mapped memeory),
消息队列(message queue)
共享内存(shared memory)
信号量(semaphore)
信号(signal)
文件(file)
套接字(Socket):这个在自己工作时工作使用的比较多。
环境变量
一些demo代码就放到了gitgee上面
test_soure_code: 记录一些平时的demo代码
六、线程间通信:
主线程做自己的事情,生成2个子线程,task1为分离,任其自生自灭,而task2还是继续送外卖,需要等待返回。(因该还记得前面说过僵尸进程吧,线程也是需要等待的。如果不想等待,就设置线程为分离线程)
注:参考资料
参考到以下博客或资料连接: