程序员的自我修养 读书笔记
第一章
进程和线程
一个标准的线程由 线程ID, 当前指令指针(PC), 寄存器集合, 堆栈组成
各个进程独享的资源有
- 栈
- 寄存器
各个线程间共享程序的内存空间和一些进程间级的资源,具体来说是
- 代码段
- 数据段
- 堆
c程序员角度来看
线程私有 | 线程间共享 |
---|---|
局部变量 | 全局变量 |
函数参数 | 堆上的数据 |
TLS数据 | 函数里的静态变量 |
程序代码 | |
打开的文件(进程级资源) |
这个说A线程打开的文件,B线程可以进行读写
windows多线程
CreateProcess 创建进程
CreateThread 创建线程
Linux多线程
没有明确的进程和线程的概念
所有执行实体是叫做任务(Task), 每个任务相当于是一个单线程的进程, 不同任务可以选择
共享内存空间,共享了内存空间的Task就相当于是构成了一个进程,这些Task相当于是线程
系统调用 | 作用 |
---|---|
fork | 复制当前进程 |
exec | 使用新的可执行映像覆盖当前可执行映像 |
clone | 创建子进程并从指定位置开始执行 |
fork产生一个完全一样的新进程
pid_t pid;
if(pid = fork()) {
...
}
fork 和 exec 一起使用可以产生新的任务fork
产生的新进程和原来的进程共享写时复制
的内存空间,当对内存修改的时候才进行内存空间复制
exec
Linux下不存在exec这个函数…所以直接exec会报错
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 execvpe(const char *file, char *const argv[], char *const envp[]);
l
: 可变长参数p
: 会搜索环境变量找到file
e
: 可自设环境变量
另外还有一个系统调用
execve
int execve(const char *filename, char *const argv[], char *const envp[]);
上面的6个exec系列是包装了execve
exec 和 fork一起使用
/* **********************************************
Auther: haibin
Created Time: 2017年08月05日 星期六 16时08分02秒
File Name : thread.c
*********************************************** */
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
int main() {
pid_t pid = fork();
int i;
if(pid < 0) {
printf("fork Error\n");
} else if(pid == 0) {
printf("I'm son\n");
execve("/bin/sh", "sh", NULL);
for(i = 0; i < 10; i++) {
printf("I'm son\n"); //不会执行
}
} else {
printf("I'm father\n");
wait();
for(i = 0; i < 10; i++) {
printf("I'm father\n"); //会执行
}
exit(0);
}
return 0;
}
exec 和 system
system相当于是重新开了一个进程,对于原来进程逻辑没有影响
所以上面的例子里如果不用execve而是用了system("bin/sh")
的话,下面的i'm son
也还是会输出的
int system(const char *command);
相当于执行了/bin/sh -c command
system在执行时候相当于会调用fork
, execve
, waitpid
system("command")
相当于是
pid_t pid = fork();
if(pid < 0) {
... //创建失败
} else if(pid == 0){
// 新任务
execl("bin/sh", "sh", "-c", command, NULL);
...
} else {
...
// 这里是原任务, pid 是新任务的pid
}
是不是有点迷之奇怪…
fork调用后
本任务的fork会返回新任务的pid
新任务的fork会返回0
所以使用fork和exec新建进程其实就是这个样子写的…
线程安全
多线程对于可共享的变量的读写可能导致数据的不一致性
- 使用操作系统提供的原子操作
- 使用锁
- 使用可重入的函数
volatile
这个..其实面试的时候被问到了这个问题,我不会,惨…
这个关键字是为shi了tu阻止过度优化而造成的线程安全问题
具体可以做到
- 阻止编译器为了提高速度将一个变量缓存到寄存器而不写回
- 阻止编译器调整操作volatile变量的指令顺序
但即使volatile能阻止编译器调整顺序,也不能够阻止CPU动态调度,所以不能完全解决这个由于优化导致的线程安全问题
第二章 静态链接
四步走
gcc其实是包装了预编译器cc1, 汇编器as, 链接器ld这些,根据参数不同调用不同的程序
预编译(Prepressing)
$gcc -E hello.c -o hello.i
或者
$cpp hello.c > hello.i
展开宏定义和处理其他以
#
开头的预编译指令
但保留#pragma
, 因为在编译时候还要用到编译(Compliation)
结果一堆复杂的分析(语法,词法,语义…)产生汇编代码$gcc -S hello.i -o hello.S
或者
$gcc -S hello.c -o hello.S
汇编(Assembly)
汇编代码转化为机器代码, 这个步骤比较简单$gcc -c hello.S -o hello.o
产生了目标文件
也可以从源文件直接过来$gcc -c hello.c -o hello.o
链接(Linking)
为什么不由汇编直接输出可执行文件而是输出一个目标文件
目标代码中有变量定义在其他模块,这些变量的地址在编译期间不能确定的