Skip to content

Latest commit

 

History

History
executable file
·
328 lines (200 loc) · 10.2 KB

unix环境高级编程.md

File metadata and controls

executable file
·
328 lines (200 loc) · 10.2 KB

unix 环境高级编程

目录


文件IO

linux对于文件io提供的系统调用大概是5个,分别是 open,read,write,lseek,close 。

linux可以把所有的东西都可以看成文件,对于一个文件,基本的操作就是可读,可写,可控制,可关闭。

  • 关于write

    • write 函数的流程

      调用write函数的将数据写入磁盘的过程

      进入内核空间 --> 将数据写入内核空间里面的缓冲区 --> 写队列 --> 磁盘

      一般默认的write函数,只负责将数据写到内核空间的缓冲区里面,然后返回,它并不等待写磁盘的操作结束。 系统的守护进程会周期的调用sync函数,将内核空间的write缓冲区的数据放到写队列。fsync函数可以确保修改过的数据,直到写到磁盘上面再返回。

    • write 函数的追加写

      write函数的追加写的实现是一个原子操作,因为如果两个进程同时打开一个文件,近些追加写的时候。如果获得 文件偏移 和 写操作 这个两个操作是独立的话,那么就可以出现被覆盖的情况。所以,write的追加写的操作是一个原子的操作。

  • 关于read

  • 其他的api

    dup/dup2  #复制fd
    fcntl     #用于文件的属性控制
    ioctl     #用于文件的属性控制
    
    • dup/dup2的用法

      todo

    • fcntl的用法:

      网络编程里面设置非阻塞

  • 进程打开文件对应的数据结构

    进程打开文件对应的数据结构,如下图所示:

文件和目录

  • linux 的文件类型

    • 普通文件

    • 目录文件

    • 块特殊文件

      磁盘文件就是块特殊文件。

    • 字符特殊文件

      在系统中,所有设备,要不是字符特殊文件,要不就是块特殊文件。比如终端输入就是字符特殊文件。

    • FIFO

    • SOCKET

    • 符号链接

  • 目录

    每一个进程都一个自己当前的工作目录,这个目录也是搜索所有相对路径名的起点。 附加一个问题,自己可以去实现cd命令。

  • linux 文件系统

    todo

IO标准库

  • 缓冲

    标准io库提供缓冲的目的是为了尽可能的减少调用read 和 write 的次数。于是,标准io库提供了缓冲的机制。标准io库的缓冲机制分为三种:

    • 全缓冲

      全缓冲表示直到写满缓冲区的时候,才进行实际的io操作。

    • 行缓冲

      行缓冲表示遇到/n就进行fflush的操作,或者是达到了行缓冲的大小,就执行fflush(好像默认是1024)。这个是stdin 和 stdout的默认操作行缓冲。

    • 不带缓冲

      不缓冲就是不缓冲。stderr就是不缓冲。

    (这个说的stdin,stdout 是行缓冲 指的是他们默认连接至终端)

  • 关于 stdin , stdout , stderr

    stdin,stdout,stderr默认是连接至终端,也就是从终端获取输入和输出。但是也可以通过重定向修改默认的路径。

    ./a.out < /etc/passwd  > std.out 2 > std.err
    
    # 这个表示 
    # stdin 是从 /etc/passwd 获得输入
    # stdout 输出道std.out 文件里面
    # stderr 输出道std.err 文件里面
    

    如果stdin,stdout被重定向到文件,那么它们就变成全缓冲了。

    #include <iostream>                                                                              #include <string>
    using namespace std;
    
    int main()
    {
        string str;
        cin>>str;
        cout<<str<<endl;
        return 0;
    }
    
    # 将输入重定向到1.txt ,将输出重定向到 out.txt
    ./a.out < 1.txt > out.txt
    

进程环境

  • 程序启动

  • 进程终止

进程控制

  • 理解fork

    • 父进程打开三个文件,然后fork出子进程,然后两个进程一个写文件,会发生什么?

      fork之后,父进程和子进程是共享同一个文件偏移量的。如果子进程写了文件之后,写完之后,会修改文件的偏移。于是,父进程再写的时候,就会在新的偏移后面写。(但是这个只能保证不会出现父子进程写的内容相互覆盖,并不能保证内容的顺序)

      比如,子进程写的是abc,父进程写的是123。可能出现的输出是a123bc,或是是1ab23c 等。但是不会出现覆盖的情况,输出的长度一定是6。

    • fork 和 vfork的区别?

      区别:

      • 没有完全拷贝父进程的进程空间里面的全部内容
      • 子进程先执行
    • 进程调用

      • nice 值

线程

linux 早期是不支持线程的,直到2.6的版本才开始支持线程库。现在linux的线程库NPTL(native posix thread library), NPTL现在是glibc的一个部分。

关于线程同步,本来是单独写了一个md文件,写了一个使用底层api的例子

线程同步

  • 线程模型

    线程可以分成两种,一种是内核线程,一种是用户线程。内核线程,由内核来调度,用户线程,由线程库来进行调度。两种模型,各有利弊。NPTL的实现方式是,每个线程都是内核线程。

  • 锁的类型

    • 互斥锁

    • 读写锁

    • 自旋锁

  • POSIX信号量

    在linux上面,信号量的api有两组。第一组是IPC的信号量,用于进程之间通信。第二组是POSIX信号量,主要用于线程同步的。这两组api的名字虽然很像,但是语义完全不一样。

    POSIX的信号量

    sem_init();
    sem_destory();
    sem_wait();
    sem_trywait();
    sem_post();
    

    IPC的信号量

    semget()
    semctl()
    
  • 条件变量

    举个小例子,更好理解。 https://github.com/zhaozhengcoder/CoderNoteBook/blob/master/example_code/condition.cc

  • 屏障

  • 线程的属性

    • 分离属性

      以分离属性启动的线程退出的时候,将自行释放其占用的资源。不需要主线程调用pthread_join来回收。

    • 线程堆栈的起始地址和大小

      linux 默认为每个线程分配了足够的堆栈空间8MB , 可以使用ulimit -a 查看。

    • 线程的优先级

      schedparam

    • 线程的策略

      schedpolicy

  • 可重入

    先举一个例子,比如信号的处理函数,信号的发生是不会提前直到的。比如,程序执行到write一个文件,第一步修改偏移,第二步是开始写入数据。

    但是,比如在完成第一步的时候,信号发生。但是,在信号处理函数中,再一次修改的了write的偏移。于是,在信号处理函数返回的时候,就会出现问题。

    一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS 调度下去执行另外一段代码,而返回控制时不会出现什么错误。

    参考:

    https://www.cnblogs.com/wuchanming/p/4020184.html

    https://www.ibm.com/developerworks/cn/linux/l-reent.html

  • 线程的私有数据

信号

信号是有用户,系统或者是进程发送给目标进程的信息,以通知目标进程某个状态已经发生改变或系统的异常。

  • linux信号可以由如下的条件产生:

    • 对于前台进程,用户可以通过终端的输入来发送信号。比如ctrl+c , 或者 kill 命令发送信号。
    • 系统异常的时候,会产生信号 (比如,非法的内存访问)
    • 进程调用api,产生信号。如果调用alarm函数,产生sigalarm信号;或是是调用kill函数,产生信号。
  • 信号的处理:

    • 对于一些信号,有它系统默认的处理方式
    • 对于一些信号,用户可以自定义信号的处理函数来进行处理
  • 常用的信号

    SIGKILL
    SIGALRM
    SIGTERM
    SIGSTP
    SIGCHLD
    SIGPIPE
    SIGURG
    
  • 信号处理函数

    #include<unistd.h>
    #include<signal.h>
    void handler() 
    {
        printf("hello\n");
    }
    main()
    {
        // 注册对SIGALRM的处理函数
        signal(SIGALRM,handler);
        alarm(5);
        for(int i=1; i<7; i++)
        {
            printf("sleep %d ...\n",i);
            sleep(1);
        }
    }
    
  • 信号处理的编程模型 —— 统一事件源

    信号处理有一个问题就是,信号处理函数要尽可能快的执行完毕,因为在信号处理期间,以确保该信号不被屏蔽很久。

    一种典型的方式是,把信号主要处理的逻辑放在程序的主循环里面,主循环再根据接受到的信号选择相应的逻辑代码,选择相应的逻辑函数进行处理。信号处理函数通过管道将信号“传递”给主循环。 具体可以看 游双的《linux服务器高性能编程》 p184页。

文件锁

文件锁除了可以给文件加锁之外,另外还可以用于进程之间的同步,比如两个进程都需要访问一块共享内存,这样就会出现类似于线程之间“race condition”的情况。这样,实现一个进程级别的互斥锁

记录锁不仅仅可以用来同步不同进程对同一文件的操作,还可以通过对同一文件加记录锁,来同步不同进程对某一共享资源的访问,如共享内存,I/O设备。

  • 文件锁

    这里有一个文件锁的小例子

    https://github.com/zhaozhengcoder/CoderNoteBook/blob/master/example_code/apue/file_lock.c

    这里可以思考一个问题,nginx的work进程之间的互斥锁是如何实现的?

    我看了一下nginx实现的这一部分,nginx的锁是自己实现的,原理好像是两种方式:

    • 共享内存

      线程中实现锁就是通过一个共享的堆上的内存(通过malloc实现),进程中实现锁也是通过这样一个共享的区域来实现进程的同步。说白了就是共享一个变量,然后通过这个变量来控制多个进程同步运行

    • 文件锁

  • 记录锁