程序员的自我修养 读书笔记

Author Avatar
Aryb1n 8月 05, 2017

第一章

进程和线程

一个标准的线程由 线程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新建进程其实就是这个样子写的…

线程安全

多线程对于可共享的变量的读写可能导致数据的不一致性

  1. 使用操作系统提供的原子操作
  2. 使用锁
  3. 使用可重入的函数

volatile

这个..其实面试的时候被问到了这个问题,我不会,惨…
这个关键字是为shi了tu阻止过度优化而造成的线程安全问题
具体可以做到

  • 阻止编译器为了提高速度将一个变量缓存到寄存器而不写回
  • 阻止编译器调整操作volatile变量的指令顺序

但即使volatile能阻止编译器调整顺序,也不能够阻止CPU动态调度,所以不能完全解决这个由于优化导致的线程安全问题

第二章 静态链接

四步走

gcc其实是包装了预编译器cc1, 汇编器as, 链接器ld这些,根据参数不同调用不同的程序

  1. 预编译(Prepressing)

    $gcc -E hello.c -o hello.i
    

    或者

    $cpp hello.c > hello.i
    

    展开宏定义和处理其他以#开头的预编译指令
    但保留#pragma, 因为在编译时候还要用到

  2. 编译(Compliation)
    结果一堆复杂的分析(语法,词法,语义…)产生汇编代码

    $gcc -S hello.i -o hello.S
    

    或者

    $gcc -S hello.c -o hello.S
    
  3. 汇编(Assembly)
    汇编代码转化为机器代码, 这个步骤比较简单

    $gcc -c hello.S -o hello.o
    

    产生了目标文件
    也可以从源文件直接过来

    $gcc -c hello.c -o hello.o
    
  4. 链接(Linking)

    为什么不由汇编直接输出可执行文件而是输出一个目标文件
    目标代码中有变量定义在其他模块,这些变量的地址在编译期间不能确定的