C++经典书籍读书笔记

目录

Essential C++

C++ Primer

基础知识

类设计者的工具

Effective C++

Linux高性能服务器编程

TCP/IP协议族及各种重要的网络协议

服务器编程

优化和检测服务器性能

跟我一起写Makefile

UNIX环境高级编程


省略了基础的C++知识,持续更新中

Essential C++

  • stl:standard template library:包括容器:vector,list,set,map和操作这些容器的泛型算法:find(),soryt(),replace(),merge()等等
  • iterator:泛型指针 迭代器
  • inline函数应该在头文件中定义
  • 类的构造函数和析构函数
  • copy constructor有些情况下必须使用,例如类中涉及数组指针,当数组内存被释放时,此时指针就指向空地址,这是不被允许的行为
  • 类可以将其他函数或者是类指定为friend,这样就具备了与成员函数相同的访问权限,可以访问类的私有成员
friend int operator(const triangular_iterator&rhs)
{
    //函数
}

class Triangular{
friend class Triangular_iterator;
//class,这样Triangular就成为了Triangular_iterator的friend,可以访问其私有变量
//但是如果Triangular_iterator提供了共有函数访问私有变量,也可以不建立朋友关系
}
  • oop:面向对象编程
  • 以对象为基础的编程无法解决类间的关系问题,这也是oop被提出来的原因
  • 面向对象编程两个最主要的特质:继承和多态
  • 动态绑定是面向对象编程风格的第三个独特概念:当函数执行时,才明白实际调用的究竟是哪一个派生类的函数,编译器无法事先确定,解析对象实际调用哪一个函数延迟到运行时进行,这就叫动态绑定
  • 多态和动态绑定的特性只有在使用pointer或reference时才发挥作用
  • 默认情况下,成员函数的解析都在编译时静态地进行,如果想要其在运行时动态进行,就需要在成员函数生命前加上关键字virtual
class Book:public Libmat{
//Book继承Libmat
}
  • 被声明为protected的成员可以被派生类直接访问,除此之外,都不能直接访问protected成员,派生类的构造函数和基类的构造函数都会执行。
  • static成员函数无法被声明为虚拟函数
  • 如果某个操作行为在基类之外不需要被用到,就将其声明为private。该基类的派生类也无法访问基类中的private number
  • 继承类必须对基类的纯虚函数提供对应的实现
  • 每当派生类中有某个成员与基类的成员同名,便会掩盖住基类的那部分成员
  • reference永远无法代表空对象,pointer可能是null,所以当使用reference时,我们不必再检查它是否是null
  • 构造函数:基类->派生类 析构函数:派生类->基类 调用顺序
  • 如果派生类把基类的虚函数完全不变地继承,那么这个派生类也会成为抽象类,不能为它定义任何对象,覆盖基类所提供的虚函数,派生类则需要提供新的定义,函数原型必须完全符合基类所声明的函数原型,包括参数列表、返回类型、常量性(const)。但是有个例外,当基类的虚函数返回某个基类形式(通常是pointer或reference),派生类中的同名函数可以返回该基类所派生出来的类型
  • 在C++中,唯有用基类的pointer或reference才能支持面向对象编程概念
  • static_cast无条件类型转换,dynamic_cast动态转换(转换时会进行类型检查)

以template进行编程

  • template模板,将实际类型抽象出来,由用户指定最终类型。代码前加上
template <typename valType>
//下文中所有指代类型的地方都用valType指代即可
class BTnode{
public:
	//...
private:
	valType _val;//栗子
};
//进行类型绑定
BTnode< int >bti;
BTnode< string >bts;

异常处理机制

  • throw抛出异常 catch捕获异常 无法处理时,可能还需要重新抛出异常throw
try
    {
        //
    }
catch(xx)
    {
    //如果try中有异常,catch中语句会进行捕捉
    }//如果一直没有对应的catch子句,函数调用链会一直不断解开,如果回溯到main还是没有,C++会调用
//标准库中的terminate()函数——默认行为是中断整个程序的执行
  • 在异常处理机制终结某个函数之前,C++保证函数中的所有局部对象的destructor都会被调用。在构造函数中进行资源分配,在析构函数中进行资源回收

C++ Primer

基础知识

  • 以0开头的数字表示八进制 024
  • 如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体外的变量被初始化为0
  • 用户自定义的类名首字母大写
  • 引用必须被初始化,相当于取了一个别名。引用不是对象
  • void*是一种特殊的指针类型,可用于存放任何对象的地址。对于这个地址中到底是一个什么类型的对象我们并不了解。因此void*的用处有限:拿它和别的指针比较、作为函数的输入或者输出、或者赋给另外一个void*指针。不能直接操作void*指针所指向的对象,因为不知道对象到底是什么类型
  • *是跟在变量上的,而不是跟在类型上。int*p1,p2 p2是int类型
  • const变量默认只在文件内有效,当多个文件中出现了同名的const变量时,等同于分别在不同的文件中分别定义了独立的变量。为了使得const变量在不同文件中都有效,可以使用extern关键字,这样只需要定义一次就行
  • 常量引用可以绑定非常量变量,但是不能通过该引用去改变所引用对象的值
  • 同理,常量地址可以指向非常量变量,但是不能通过该常量地址去改变所指对象的值

指向常量的指针或引用,不过是指针或引用“自以为是”罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象的值

int a=0;
int *const pa=&a;//pa将一直指向a
const int b=2;
const int *const pb=&b;//pb是一个指向常量对象的常量指针
  • *后面的const保证本指针一直指向同一个位置,*前面的const保证本指针不会试图去改变所指的对象
  • 函数体内定义的变量一般来说并非存放在固定地址中,定义于所有函数体之外的对象其地址固定不变
using db = double;
db x = 10.2;//相当于typedef

  • 迭代器的.end()返回的是指向容器尾元素的下一位置的迭代器,是一个本不存在的尾后元素。
  • 凡是用了迭代器的循环体,都不要向迭代器所属的容器添加元素
  • 不能用一个数组初始化另一个数组,也不能把一个数组直接赋值给另一个数组
  • 可以用数组初始化vector,但是不能用vector初始化数组
int a[]={0,1,2,3,4};
vector<int> b(begin(a),end(a));//用数组初始化vector
  • 严格意义上来说,C++语言中没有多维数组,通常所说的多维数组其实是数组的数组
assert(expr);
//首先对expr求值,如果表达式为假,assert输出信息并终止程序的执行
//如果表达式为真,assert什么都不做
  • 如果函数的实参数量未知带式全部实参的类型都相同,我们可以用initializer_list类型的形参,用于表示某种特定类型的值的数组
  • 如果在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体
  • inline内联函数:将它在每个调用点上展开。内联机制用于优化规模较小、流程直接、频繁调用的函数。
  • constexpr函数是指能用于常量表达式的函数,也就是返回值是一个常量
  • 内联函数和constexpr函数可以在函数中多次定义,因为编译器想要展开函数仅有函数声明是不够的,所以推荐将这两个函数的定义放在头文件中
  • assert预处理宏:所谓预处理宏其实是一个预处理变量,其行为类似于内联函数
assert(expr);
//首先对expr求值,如果表达式为假,assert输出信息并终止程序的执行
//如果表达式为真,assert什么都不做
  • 编译器提供默认的拷贝、赋值、销毁的操作,但是有些情况下默认的这些操作无法正常运行,需要我们自定义,尤其是当类需要分配类对象之外的资源时
//构造函数
person(int age1,int grade1):age(age1),grade(grade1)
{
}
//如果我们定义了构造函数,编译器将不会自动生成默认的构造函数,如果需要默认的构造函数,需要使用dafault
person()=default;
  • 类的默认访问权限是private,结构体的默认权限是public。使用class和struct定义类的唯一区别就是默认的访问权限
  • 类可以运行其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元
  • 一个可变成员函数(mutable data member)永远不会是const,即使它是const对象的成员。一个const成员函数可以改变一个可变成员的值
  • 友元函数能够定义在类的内部,这样的函数是隐式内联的
class A{
friend class B;
}//B被声明为A的友元,所以B可以访问A的成员
//友元函数可以在类的内部定义,但是一定要在类外进行声明,不然直接进行调用会被认为没有声明
  • 如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值
  • 对于没有传入参数的构造函数来说,创建一个对象时括号要省略,不然会被当成声明一个函数
  • 聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。当一个类满足如下条件时,我们说它是聚合的:所有成员都是public的、没有定义任何构造函数、没有类内初始值、没有基类,也没有virtual函数。可以提供一个花括号括起来的成员初始化列表,并用它初始化聚合类的数据成员,初始值的顺序必须与声明的顺序一致
  • 静态成员不属于类的某个对象,但是仍然可以使用类的对象、引用或者指针来访问静态成员,成员函数不用通过作用域运算符就能直接使用静态成员
  • 在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句
  • 必须在类的外部定义和初始化每个静态成员
  • endl换行并且刷新缓冲区,ends向缓冲区插入空字符并刷新缓冲区,flush刷新缓冲区但不输出任何额外的字符

  • forward_list的设计目标是达到与最好的手写的单向链表数据结构相当的性能,因此forward_list没有size操作
  • 每个容器类型都支持相等运算符(==和!=);除了无序关联容器外的所有容器都支持关系运算符(>,>=,<,<=)。关系型运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素
  • 当用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象的一个拷贝,而非对象本身
  • 对于一个空容器调用front和back,就像使用一个越界的下标一样,是一种严重的程序设计错误
  • erase()从容器中指定位置删除元素,返回指向删除的元素之后位置的迭代器
  • 调用erase之后不必递增迭代器,因为erase返回的迭代器已经指向序列中下一个元素。调用insert之后,需要递增迭代器两次,insert在给定位置之前插入新元素,然后返回指向新插入元素的迭代器。
  • 添加/删除vector或string的元素后,或在deque中首元素之外任何位置添加/删除元素后,原来end返回的迭代器总是会失效。因此,添加或删除元素的循环程序必须反复调用end,而不能在循环之前保存end返回的迭代器,一直当作末尾使用。
  • capacity()操作告诉我们容器在不扩张内存空间的情况下可以容纳多少个元素,reserve()操作允许我们通知容器它应该准备保存多少个元素
  • string的find("str")函数完成最简单的搜索,返回str在string中第一次出现的下标
  • 适配器(adaptor)是标准库中的一个通用概念,一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物。一个适配器接受一个已有的容器类型,使其行为看起来像一种不同的类型
  • 并不是任何容器都能使用适配器”转换成“另一种容器
  • 泛型算法本身不会执行容器的操作,它们只会运行在迭代器之上,执行迭代器的操作。泛型算法运行于迭代器之上而不会执行容器操作的特性带来了一个令人惊讶但非常必要的编程设定:算法永远不会改变底层容器的大小。算法可能改变容器中保存的元素的值,也可能在容器内移动元素,但是永远不会直接添加或删除元素
  • 一些算法从两个序列中读取元素。构成这两个序列的元素可以来自不同类型的容器
  • 为了消除重复单词,首先将vector排序,使得重复的单词相邻出现,然后再用unique()重新排列vector,使得不重复的元素出现在vector的开始部分
  • 一个lambda表达式表示一个可调用的代码单元。可以将其理解为一个未命名的内联函数
[capture list] (parameter list)->return type{function body}
    //一个lambda表达式的形式
    //capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表,通常为空
    //return type,parameter list和function body与普通函数一样,分别表示返回类型,参数列表和函数体
    //与普通函数不同,lambda必须使用尾置返回
    //一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量
	//lambda用来向函数传递
  • lambda变量捕获的方式可以是值或者引用。与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝,因此随后对被捕获变量的修改不会影响到lambda内对应的值
  • [=]隐式捕获列表,采用值捕获方式,[&]隐式捕获列表,采用引用捕获方式
  • 默认情况下,对于一个值被拷贝的变量,lambda不会改变其值,如果希望改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable
  • 多数链表特有的算法都与其通用版本很类似,但是不完全相同。链表特有版本与通用版本之间的一个至关重要的区别是链表版本会改变底层的容器
  • 谓词:返回可以转换为bool类型的值的函数。泛型算法通常用来检测元素。标准库中使用的谓词是一元
  • 关联容器:容器中的元素按照关键字来保存和访问,而顺序容器中的元素是按照它们在容器中的位置来顺序保存和访问的
  • 两个主要的关联容器:map,set
  • 允许重复关键字的容器的名字都包含单词multi,不保持关键字按顺序存储的容器的名字都以单词unordered开头
  • map,set,multimap,multiset,unordered_map,unodered_set,unordered_multimap,unordered_multiset
map<string,size>word_count;
string word;
set<string>exclude={"the","but","and","or","an"}
while(cin>>word)
    if(exclude.find(word)==exclude.end())
    ++word_count[word];
for(const auto &w:word_count)
    cout<<w.first<<" occurs "<<w.second<<endl;
//find调用返回一个迭代器,如果给定关键字在set中,迭代器指向该关键字,否则find返回尾后迭代器
map<string,string>authors={
   
   {"joyce","james"},{"austen","jane"},{"dickens","charles"}}
  • 自定义操作时,必须在<>中写明
  • set的迭代器是const
  • 添加单一元素的insert和emplace版本返回一个pair,告诉我们插入操作是否成功。pair的first成员是一个迭代器,指向具有给定关键字的元素,second成员是一个bool值,指出元素是否插入成功还是已经存在于容器中
map<string,size>word_count;
string word;
while(cin>>word)
    {
        auto ret=word_count.insert({word,1});
        if(!ret.second)
            ++ret.first->second;
    }
//统计单词出现次数
  • c.erase(k):从c中删除每个关键字为k的元素。返回一个size_type值,指出删除元素的数量
c[k];
//返回关键字为k的元素;如果k不在c中,添加一个关键字为k的元素,对其进行值初始化
c.at(k);
//访问关键字为k的元素,带参数检查,若k不在c中,抛出一个out_of_range异常
c.lower_bound(k);
//返回一个迭代器,指向第一个关键字不小于k的元素
c.upper_bound(k);
//返回一个迭代器,指向第一个关键字大于K的元素
c.equal_range(k);
//返回一个迭代器pair,表示关键字等于k的元素的范围。若k不存在,pair两个成员均等于c.end()
//lower_bound()和upper_bound()不适合用于无序容器
//下标和at操作只适用于非const的map和unordered_map
  • 对map使用下标操作时,如果关键字不存在,会插入一个新元素,其关键字为给定关键字,其值为0.如果只想知道一个给定关键字是否在map中,应该使用find()
  • 无序容器在存储上组织为一组桶
  • 静态内存保存局部static对象、类static数据对象以及定义在任何函数之外的变量
  • 栈内存用来保存定义在函数内的非static对象
  • 分配在静态或者栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在。static对象在使用前分配,在程序结束时销毁
  • 除了静态内存和栈内存,每个程序还拥有一个内存池,这部分内存被称作自由空间或堆。程序用堆来存储动态分配的对象——即那些在程序运行时分配的对象。动态对象的生存期由程序控制。当动态对象不再使用时,我们的代码必须显式地销毁它们
  • 智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。shared_ptr允许多个指针指向同一个对象,unique_ptr独占指向的对象
shared_ptr<string>p1;
//指向string
shared_ptr<list<int>>p2;
//指向int的list
  • 当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过另一个特殊的成员函数——析构函数完成销毁工作的
  • 用new分配const对象是合法的
  • 编译器不能分辨一个指针指向的是静态还是动态分配的对象。类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放
  • unique_ptr不支持普通的拷贝或者赋值操作。例外是可以拷贝或者赋值一个将要被销毁的unique_ptr
  • weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数
  • 创建一个weak_ptr时,要用一个shared_ptr来初始化它
  • weak_ptr可能指向一个空对象,所以不能通过weak_ptr直接访问对象,要使用到lock()函数
  • 动态分配空数组是合法的
  • unique_ptr支持管理动态数组,shared_ptr不支持
  • 类似vector,allocator是一个模板
allocator<string>alloc;
auto const p=alloc.allocate(n);
//allocate调用为n个string分配了内存
//allocator分配的内存是未构造的,需要在此内存中构造对象
auto q=p;//q指向最后构造的元素之后的位置
alloc.construct(q++);//*q为空字符串
alloc.construct(q++,10,'c');//*q为cccccccccc
alloc.construct(q++,"hi");//*q为hi
while(q!=p)
    alloc.destroy(--q);
//使用完对象后,必须对每个构造的元素调用destroy来销毁它们

类设计者的工具

Effective C++

  • 尽量以const、enum、inline替换#define
  • 尽可能使用const
char greeting[]="hello";
const char*p=greeting;//non-const pointer,const data
char *const p=greeting;//const pointer,non-const data
  • const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量

Linux高性能服务器编程

TCP/IP协议族及各种重要的网络协议

  • 数据链路层、网络层、传输层协议是在内核中实现的,操作系统需要实现一组系统调用,使得应用程序能够访问这些协议提供的服务。实现这组系统调用的API是socket
  • UDP无须为应用层数据保存副本, 因为它提供的服务是不可靠的。 如果应用程序检测到该数据报未能被接收端正确接收, 并打算重发这个数据报, 则应用程序需要重新从用户空间将该数据报拷贝到UDP内核发送缓冲区中。
  • 通常ARP维护一个高速缓存,其中包含经常访问或最近访问的机器的IP地址到物理地址的映射。这样避免了重复的ARP请求。linux下可以用arp命令查看和修改ARP高速缓存
  • ARP请求和应答是从以太网驱动程序发出的
  • 域名查询服务有多种实现方式:NIS(网络信息服务),DNS和本地静态文件等
  • linux使用/etc/resolv.conf文件来存放DNS服务器的IP地址
  • 使用host+网址查询对应IP地址
  • 由socket定义的这一组API提供如下两点功能:一是将应用程序数据从用户缓冲区中复制到TCP/UDP内核发送缓冲区,以交付内核来发送数据,或者是从内核TCP/UDP接收缓冲区中复制数据到用户缓冲区,以读取数据。二是应用程序可以通过它们来修改内核中各层协议的某些头部信息或者其他数据结构,从而精细的控制底层通信的行为
  • 同一个数据报的分片的标识值是一样的
  • 可以使用route命令或netstat命令查看路由表
  • TCP连接的任意一端在任一时刻都处于某种状态,当前状态可以通过netstat命令查看
  • 在Linux系统上, 一个TCP端口不能被同时打开多次(两次及以上)
  • 有些传输层协议具有外带(OOB)数据的概念,用于迅速通告对方本端发生的重要事件
  • 拥塞控制算法在linux下有多种实现,比如reno算法、vegas算法和cubic算法
  • 在HTTP通信链上,客户端和目标服务器之间通常存在某些中转代理服务器,它们提供对目标资源的中转访问
  • IP头部的源端IP地址和目的端IP地址在转发过程中是始终不变的(一种例外是源路由选择) 。 但帧头部的源端物理地址和目的端物理地址在转发过程中则是一直在变化的。
  • GET、HEAD、OPTIONS、TRACE、PUT、DELETE等请求方法被认为是等幂的,即多次连续的、重复的请求和只发送一次该请求具有完全相同的效果。而POST方法则不同,连续多次发送同样一个请求可能进一步影响服务器上的资源


  •  

服务器编程

  • 现代PC大多采用小端字节序, 因此小端字节序又被称为主机字节序。
  • 当格式化的数据(比如32 bit整型数和16 bit短整型数) 在两台使用不同字节序的主机之间直接传递时, 接收端必然错误地解释之。 解决问题的方法是: 发送端总是把要发送的数据转化成大端字节序数据后再发送, 而接收端知道对方传送过来的数据总是采用大端字节序, 所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换, 大端机不转换) 。 因此大端字节序也称为网络字节序, 它给所有接收数据的主机提供了一个正确解释收到的格式化数据的保证
  • 通用socket地址结构不好用,linux为各个协议族提供了专门的socket地址结构体
  • 所有专用socket地址(以及sockaddr_storage) 类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有socket编程接口使用的地址参数的类型都是sockaddr
  • 创建socket时, 我们给它指定了地址族, 但是并未指定使用该地址族中的哪个具体socket地址。
  • 将一个socket与socket地址绑定称为给socket命名。
  • 在服务器程序中, 我们通常要命名socket, 因为只有命名后客户端才能知道该如何连接它。 客户端则通常不需要命名socket, 而是采用匿名方式, 即使用操作系统自动分配的socket地址。
  • socket被命名之后, 还不能马上接受客户连接, 我们需要使用如下系统调用来创建一个监听队列以存放待处理的客户连接:
  • accept只是从监听队列中取出连接, 而不论连接处于何种状态(如上面的ESTABLISHED状态和CLOSE_WAIT状态) , 更不关心任何网络状况的变化
  • pipe函数可用于创建一个管道, 以实现进程间通信。
  • 通过pipe函数创建的这两个文件描述符fd[0]和fd[1]分别构成管道的两端, 往fd[1]写入的数据可以从fd[0]读出。并且, fd[0]只能用于从管道读出数据, fd[1]则只能用于往管道写入数据, 而不能反过来使用。 如果要实现双向的数据传输,就应该使用两个管道
  • socketpair函数。 它能够方便地创建双向管道。
  • tee函数在两个管道文件描述符之间复制数据, 也是零拷贝操作。
  • fcntl函数, 正如其名字(file control) 描述的那样, 提供了对文件描述符的各种控制操作。
  • Linux服务器程序一般以后台进程形式运行。 后台进程又称守护进程(daemon) 。 它没有控制终端, 因而也不会意外接收到用户输入。守护进程的父进程通常是init进程(PID为1的进程) 。
  • 服务器的调试和维护都需要一个专业的日志系统。 Linux提供一个守护进程来处理系统日志——syslogd, 不过现在的Linux系统上使用的都是它的升级版——rsyslogd。
  • Linux上运行的程序都会受到资源限制的影响,Linux系统资源限制可以通过如下一对函数来读取和设置:
  • 改变进程根目录的函数是chroot
  • C/S模型的逻辑很简单。 服务器启动后, 首先创建一个(或多个)监听socket, 并调用bind函数将其绑定到服务器感兴趣的端口上, 然后调用listen函数等待客户连接。
  • 由于客户连接请求是随机到达的异步事件, 服务器需要使用某种I/O模型来监听这一事件。
  • fork()创建子进程。
  • 服务器同时监听多个客户请求是通过select系统调用实现的
  • 两种高效的事件处理模式: Reactor和Proactor。
  • 同步I/O模型通常用于实现Reactor模式, 异步I/O模型则用于实现Proactor模式。
  • Reactor是这样一种模式, 它要求主线程(I/O处理单元, 下同) 只负责监听文件描述上是否有事件发生, 有的话就立即将该事件通知工作线程(逻辑单元, 下同) 。 除此之外, 主线程不做任何其他实质性的工
    作。 读写数据, 接受新的连接, 以及处理客户请求均在工作线程中完成
  • Proactor模式将所有I/O操作都交给主线程和内核来处理, 工作线程仅仅负责业务逻辑。
  • 服务器主要有两种并发编程模式: 半同步/半异步(half-sync/half-async)模式和领导者/追随者(Leader/Followers) 模式。
  • 在I/O模型中, “同步”和“异步”区分的是内核向应用程序通知的是何种I/O事件(是就绪事件还是完
    成事件) , 以及该由谁来完成I/O读写(是应用程序还是内核) 。 在并发模式中, “同步”指的是程序完全按照代码序列的顺序执行; “异步”指的是程序的执行需要由系统事件来驱动。
  • 半同步/半异步模式中, 同步线程用于处理客户逻辑, 相当于图8-4中的逻辑单元; 异步线程用于处理I/O事件, 相当于图8-4中的I/O处理单元。
  • 领导者/追随者模式是多个工作线程轮流获得事件源集合, 轮流监听、 分发并处理事件的一种模式。
  • 领导者/追随者模式包含如下几个组件: 句柄集(HandleSet) 、 线程集(ThreadSet) 、 事件处理器(EventHandler) 和具体的事件处理器(ConcreteEventHandler) 。
  • 高效的逻辑处理方式——有限状态机
  • 以空间换时间, 即“浪费”服务器的硬件资源, 以换取其运行效率。 这就是池(pool) 的概念。
  • 池是一组资源的集合, 这组资源在服务器启动之初就被完全创建好并初始化, 这称为静态资源分配。
  • I/O复用虽然能同时监听多个文件描述符, 但它本身是阻塞的。
  • Linux下实现I/O复用的系统调用主要有select、 poll和epoll
  • select系统调用的用途是: 在一段指定时间内, 监听用户感兴趣的文件描述符上的可读、 可写和异常等事件。
  • 网络程序中, select能处理的异常情况只有一种: socket上接收到带外数据。
  • poll系统调用和select类似, 也是在指定时间内轮询一定数量的文件描述符, 以测试其中是否有就绪者。
  • epoll是Linux特有的I/O复用函数。
  • 首先, epoll使用一组函数来完成任务, 而不是单个函数。其次, epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中, 从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。 但epoll需要使用一个额外的文件描述符, 来唯一标识内核中的这个事件表。
  • epoll系列系统调用的主要接口是epoll_wait函数。 它在一段超时时间内等待一组文件描述符上的事件
  • epoll对文件描述符的操作有两种模式: LT(Level Trigger, 电平触发) 模式和ET(Edge Trigger, 边沿触发) 模式。 LT模式是默认的工作模式, 这种模式下epoll相当于一个效率较高的poll。 当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时, epoll将以ET模式来操作该文件描述符。 ET模式是epoll的高效工作模式。
  • 我们期望一个socket连接在任一时刻都只被一个线程处理。 这一点可以使用epoll的EPOLLONESHOT事件实现。
  • 当一个线程在处理某个socket时, 其他线程是不可能有机会操作该socket的。 但反过来思考, 注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕, 该线程就应该立即重置这个socket上的
    EPOLLONESHOT事件, 以确保这个socket下一次可读时, 其EPOLLIN事件能被触发, 进而让其他工作线程有机会继续处理这个socket。
  • 在实际应用中, 有不少服务器程序能同时监听多个端口, 比如超级服务inetd和android的调试服务adbd。
  • 从bind系统调用的参数来看, 一个socket只能与一个socket地址绑定, 即一个socket只能用来监听一个端口。 因此, 服务器如果要同时监听多个端口, 就必须创建多个socket, 并将它们分别绑定到各个端口上。
  • 即使是同一个端口, 如果服务器要同时处理该端口上的TCP和UDP请求, 则也需要创建两个不同的socket: 一个是流socket, 另一个是数据报socket, 并将它们都绑定到该端口上。
  • Linux因特网服务inetd是超级服务。 它同时管理着多个子服务, 即监听多个端口。 现在Linux系统上使用的inetd服务程序通常是其升级版本xinetd。
  • 信号是由用户、 系统或者进程发送给目标进程的信息, 以通知目标进程某个状态的改变或系统异常。
  • Linux下, 一个进程给其他进程发送信号的API是kill函数。
  • 目标进程在收到信号时, 需要定义一个接收函数来处理之。
  • 要为一个信号设置处理函数, 可以使用signal系统调用
  • 设置信号处理函数的更健壮的接口是如下的系统调用: sigaction()
  • 设置进程信号掩码后, 被屏蔽的信号将不能被进程接收。 如果给进程发送一个被屏蔽的信号, 则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽, 则它能立即被进程接收
  • sigpending()可以获得进程当前被挂起的信号集
  • 当挂起进程的控制终端时, SIGHUP信号将被触发。 对于没有控制终端的网络后台程序而言, 它们通常利用SIGHUP信号来强制服务器重读配置文件。
  • strace命令能跟踪程序执行时调用的系统调用和接收到的信号。
  • 默认情况下, 往一个读端关闭的管道或socket连接中写数据将引发SIGPIPE信号。 我们需要在代码中捕获并处理该信号, 或者至少忽略它, 因为程序接收到SIGPIPE信号的默认行为是结束进程, 而我们绝对不希望因为错误的写操作而导致程序退出。
  • 内核通知应用程序带外数据到达可以使用SIGURG信号
  • 网络程序需要处理的第三类事件是定时时间。我们要将每个定时事件分别封装成定时器, 并使用某种容器类数据结构, 比如链表、排序链表、时间堆和时间轮, 将所有定时器串联起来, 以实现对定时事件的统一管理
  • Linux提供了三种定时方法, 它们是:
    ❑ socket选项SO_RCVTIMEO和SO_SNDTIMEO。这两个选项仅对与数据接收和发送相关的socket专用系统调用
    ❑ SIGALRM信号。由alarm和setitimer函数设置的实时闹钟一旦超时, 将触发SIGALRM信号。
    ❑ I/O复用系统调用的超时参数 。Linux下的3组I/O复用系统调用都带有超时参数, 因此它们不仅能统一处理信号和I/O事件, 也能统一处理定时事件。 但是由于I/O复用系统调用可能在超时时间到期之前就返回(有I/O事件发生) , 所以如果我们要利用它们来定时, 就需要不断更新定时参数以反映剩余的时间
  • 设计定时器的另外一种思路是:将所有定时器中超时时间最小的一个定时器的超时时间作为心搏间隔
  • 我们称用最小堆实现的定时器为时间堆。
  • Linux服务器程序必须处理的三类事件: I/O事件、 信号和定时事件。
  • 高性能I/O框架库Libevent
  • I/O框架库要处理的对象, 即I/O事件、 信号和定时事件, 统一称为事件源。 一个事件源通常和一个句柄绑定在一起。 句柄的作用是, 当内核检测到就绪事件时, 它将通过句柄来通知应用程序这一事件。
  • 在Linux环境下, I/O事件对应的句柄是文件描述符, 信号事件对应的句柄就是信号值。
  • I/O框架库一般将系统支持的各种I/O复用系统调用封装成统一的接口, 称为事件多路分发器。
  • 12章跳过
  • Linux下创建新进程的系统调用是fork。
  • 有时我们需要在子进程中执行其他程序, 即替换当前进程映像, 这就需要使用exec系列函数之一
  • 在子进程结束运行之后, 父进程读取其退出状态之前, 我们称该子进程处于僵尸态。
  • 管道只能用于有关联的两个进程( 比如父、 子进程) 间的通信。 而下面要讨论的3种System V IPC能用于无关联的多个进程之间的通信, 因为它们都使用一个全局唯一的键值来标识一条信道。
  • Linux信号量的API都定义在sys/sem.h头文件中, 主要包含3个系统调用: semget、 semop和semctl。
  • semget系统调用创建一个新的信号量集, 或者获取一个已经存在的信号量集。
  • semop系统调用改变信号量的值, 即执行P、 V操作。
  • semctl系统调用允许调用者对信号量进行直接控制。
  • 这些操作中, GETNCNT、 GETPID、 GETVAL、 GETZCNT和SETVAL操作的是单个信号量, 它是由标识符sem_id指定的信号量集中的第sem_num个信号量; 而其他操作针对的是整个信号量集, 此时semctl的参数sem_num被忽略。
  • shmget系统调用创建一段新的共享内存, 或者获取一段已经存在的共享内存。
  • 共享内存被创建/获取之后, 我们不能立即访问它, 而是需要先将它关联到进程的地址空间中。 使用完共享内存之后, 我们也需要将它从进程地址空间中分离。 这两项任务分别由如下两个系统调用实现:
  • shmctl系统调用控制共享内存的某些属性。
  • 消息队列是在两个进程之间传递二进制块数据的一种简单有效的方式。
  • Linux消息队列的API都定义在sys/msg.h头文件中, 包括4个系统调用: msgget、 msgsnd、 msgrcv和msgctl。
  • msgget系统调用创建一个消息队列, 或者获取一个已有的消息队列。
  • msgsnd系统调用把一条消息添加到消息队列中。
  • msgrcv系统调用从消息队列中获取消息。
  • msgctl系统调用控制消息队列的某些属性。
  • Linux提供了ipcs命令, 以观察当前系统上拥有哪些共享资源实例。
  • 传递一个文件描述符并不是传递一个文件描述符的值, 而是要在接收进程中创建一个新的文件描述符, 并且该文件描述符和发送进程中被传递的文件描述符指向内核中相同的文件表项。
  • 如何在两个不相干的进程之间传递文件描述符呢? 在Linux下, 我们可以利用UNIX域socket在进程间传递特殊的辅助数据, 以实现文件描述符的传递
  • 创建一个线程的函数是pthread_create。
  • 结束一个线程的函数是pthread_exit
  • 一个进程中的所有线程都可以调用pthread_join函数来回收其他线程(前提是目标线程是可回收的, 见后文) , 即等待其他线程结束。
  • 异常终止一个线程, 即取消线程pthread_cancel
  • 3种专门用于线程同步的机制: POSIX信号量、 互斥量和条件变量
  • 在Linux上, 信号量API有两组。 一组是System VIPC信号量, 另外一组是我们现在要讨论的POSIX信号量。
  • 常用的POSIX信号量函数是下面5个:
#include<semaphore.h>
int sem_init(sem_t*sem,int pshared,unsigned int value);
int sem_destroy(sem_t*sem);
int sem_wait(sem_t*sem);
int sem_trywait(sem_t*sem);
int sem_post(sem_t*sem);
  • 互斥锁(也称互斥量) 可以用于保护关键代码段, 以确保其独占式的访问, 这有点像一个二进制信号量。
  • POSIX互斥锁的相关函数主要有如下5个:
#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t*mutex,const
pthread_mutexattr_t*mutexattr);
int pthread_mutex_destroy(pthread_mutex_t*mutex);
int pthread_mutex_lock(pthread_mutex_t*mutex);
int pthread_mutex_trylock(pthread_mutex_t*mutex);
int pthread_mutex_unlock(pthread_mutex_t*mutex);
  • 互斥锁属性pshared指定是否允许跨进程共享互斥锁
  • 互斥锁属性type指定互斥锁的类型。
  • 条件变量用于在线程之间同步共享数据的值。 条件变量提供了一种线程间的通知机制: 当某个共享数据达到某个值的时候, 唤醒等待这个共享数据的线程。
  • 条件变量的相关函数主要有如下5个:
#include<pthread.h>
int pthread_cond_init(pthread_cond_t*cond,const
pthread_condattr_t*cond_attr);
int pthread_cond_destroy(pthread_cond_t*cond);
int pthread_cond_broadcast(pthread_cond_t*cond);
int pthread_cond_signal(pthread_cond_t*cond);
int pthread_cond_wait(pthread_cond_t*cond,pthread_mutex_t*mutex);
  • 如果一个函数能被多个线程同时调用且不发生竞态条件, 则我们称它是线程安全的(thread safe) , 或者说它是可重入函数。
  • 一些库函数之所以不可重入,主要是因为其内部使用了静态变量。不过Linux对很多不可重入的库函数提供了对应的可重入版本, 这些可重入版本的函数名是在原函数名尾部加上_r。
  • 应该定义一个专门的线程来处理所有的信号。
 
 

 

优化和检测服务器性能

  • Linux平台的一个优秀特性是内核微调, 即我们可以通过修改文件的方式来调整内核参数。
  • 在服务器的开发过程中, 我们可能碰到各种意想不到的错误。 一种调试方法是用tcpdump抓包, 正如本书前面章节介绍的那样。 不过这种方法主要用于分析程序的输入和输出。 对于服务器的逻辑错误, 更方便的调试方法是使用gdb调试器。
  • 编写压力测试工具通常被认为是服务器开发的一个部分。
  • p590跳过,gdb调试不会
  • Linux提供了很多有用的工具, 以方便开发人员调试和测评服务器程序。
  • 几个最常用的工具: tcpdump、 nc、 strace、 lsof、netstat、 vmstat、 ifstat和mpstat。
  • tcpdump是一款经典的网络抓包工具。
  • lsof( list open file) 是一个列出当前系统打开的文件描述符的工具。
  • nc( netcat) 命令短小精干,功能强大,它主要被用来快速构建网络连接。 我们可以让它以服务器方式运行, 监听某个端口并接收客户连接, 因此它可用来调试客户端程序。 我们也可以使之以客户端方式运行,向服务器发起连接并收发数据, 因此它可以用来调试服务器程序, 此时它有点像telnet程序。
  • strace是测试服务器性能的重要工具。 它跟踪程序运行过程中执行的系统调用和接收到的信号, 并将系统调用名、 参数、 返回值及信号名输出到标准输出或者指定的文件。
  • vmstat是virtual memory statistics的缩写, 它能实时输出系统的各种资源的使用情况, 比如进程信息、 内存使用、 CPU使用率以及I/O使用情况。
  • ifstat是interface statistics的缩写, 它是一个简单的网络流量监测工具。
  • mpstat是multi-processor statistics的缩写, 它能实时监测多处理器系统上每个CPU的使用情况。

跟我一起写Makefile

  • 无论是 C 还是 C++,首先要把源文件编译成中间代码文件,在 Windows 下也就是 .obj 文件, UNIX 下是 .o 文件,即 Object File,这个动作叫做编译(compile)。然后再把大量的 Object File 合成执行文件,这个动作叫作链接(link)。
  • 在编译时,编译器只检测程序语法和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成 ObjectFile。而在链接程序时,链接器会在所有的 Object File 中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error)
  • make 命令执行时,需要一个 makefile 文件,以告诉 make 命令需要怎么样的去编译和链接程序
  • prerequisites中如果有一个以上的文件比target文件要新的话, command所定义的命令就会被执行。
  • 在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个 Tab键作为开头。
  • 声明变量object,然后通过$(object)使用变量
  • 命令前面加“-”号意味着不管报错,继续执行
  • cc告知目标文件的依赖文件,并更新目标文件
  • make 支持三个通配符:* , ? 和 ~ 。
  • $?是一个自动化变量
  • 特殊变量 VPATH告诉make,如果在当前目录下找不到依赖文件和目标文件,就去VPATH所示目录去找。如果没有定义VPATH,则只会在当前目录下找
  • .PHONY”来显式地指明一个目标是“伪目标”
  • 大多数的 C/C++ 编译器都支持一个“-M”的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。
  • make 会把其要执行的命令行在命令执行前输出到屏幕上。当我们用 @ 字符在命令行前,那么,这个命令将不被 make 显示出来
  • 如果要让上一条命令的结果应用在下一条命令时,应该使用分号分隔这两条命令,而不是写在两行
  • 可以为某个目标设置局部变量,这种变量被称为“Target-specific Variable”,它可以和“全局变量”同名,因为它的作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效。而不会影响规则链以外的全局变量的值。
  • ifdef <variable-name> 如果变量 <variable-name> 的值非空,那到表达式为真。否则,表达式为假。
  • ifeq (<arg1>, <arg2>) 比较参数 arg1 和 arg2 的值是否相同。
  • ifneq (<arg1>, <arg2>) 其比较参数 arg1 和 arg2 的值是否相同,如果不同,则为真。
  • ifndef <variable-name> 与ifdef相反
  • 函数调用,很像变量的使用,也是以 $ 来标识的,其语法如下: $(<function> <arguments>)
  • $(subst <from>,<to>,<text>) 把字串 <text> 中的 <from> 字符串替换成 <to> 。
  • $(patsubst <pattern>,<replacement>,<text>) 查找 <text> 中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式<pattern> ,如果匹配的话,则以 <replacement> 替换。
  • $(strip <string>) 去掉 <string> 字串中开头和结尾的空字符
  • $(findstring <find>,<in>)
  • 在字串 <in> 中查找 <find> 字串
  • $(filter <pattern...>,<text>) 以 <pattern> 模式过滤 <text> 字符串中的单词,保留符合模式 <pattern> 的单词。可以有多个模式。
  • $(filter-out <pattern...>,<text>) 以 <pattern> 模式过滤 <text> 字符串中的单词,去除符合模式 <pattern> 的单词。可以有多个模式。
  • $(sort <list>)给字符串 <list> 中的单词排序(升序)。
  • $(word <n>,<text>) 取字符串 <text> 中第 <n> 个单词。(从一开始)
  • $(wordlist <ss>,<e>,<text>) 从字符串 <text> 中取从 <ss> 开始到 <e> 的单词串。 <ss> 和 <e> 是一个数字。
  • $(words <text>) 统计 <text> 中字符串中的单词个数
  • $(firstword <text>) 取字符串 <text> 中的第一个单词
  • $(dir <names...>) 从文件名序列 <names> 中取出目录部分。目录部分是指最后一个反斜杠(/ )之前的部分。如果没有反斜杠,那么返回 ./ 。
  • $(notdir <names...>) 从文件名序列 <names> 中取出非目录部分。非目录部分是指最后一个反斜杠(/ )之后的部分。
  • $(suffix <names...> )从文件名序列 <names> 中取出各个文件名的后缀。
  • $(basename <names...>) 从文件名序列 <names> 中取出各个文件名的前缀部分。
  • $(addsuffix <suffix>,<names...>) 把后缀 <suffix> 加到 <names> 中的每个单词后面。
  • $(addprefix <prefix>,<names...>) 把前缀 <prefix> 加到 <names> 中的每个单词前面。
  • $(join <list1>,<list2>) 把 <list2> 中的单词对应地加到 <list1> 的单词后面。如果 <list1> 的单词个数要比
    <list2> 的多,那么, <list1> 中的多出来的单词将保持原样。如果 <list2> 的单词个数要比<list1> 多,那么, <list2> 多出来的单词将被复制到 <list1> 中
  • $(foreach <var>,<list>,<text>) 把参数 <list> 中的单词逐一取出放到参数 <var> 所指定的变量中,然后再执行 <text> 所包含的表达式。每一次 <text> 会返回一个字符串,循环过程中, <text> 的所返回的每个
    字符串会以空格分隔,最后当整个循环结束时, <text> 所返回的每个字符串所组成的整个字符串(以空
    格分隔)将会是 foreach 函数的返回值。
  • $(if <condition>,<then-part>) 或者$(if <condition>,<then-part>,<else-part>)
  • $(call <expression>,<parm1>,<parm2>,...,<parmn>) 可以用来创建新的参数化的函数。
  • $(origin <variable>) 告诉你你的这个变量variable是哪里来
  • shell 函数也不像其它的函数。顾名思义,它的参数应该就是操作系统 Shell 的命令
  • make 命令执行后有三个退出码:0表示成功执行。1表示出错。如果你使用了 make 的“-q”选项,并且 make 使得一些目标不需要更新,那么返回 2
  • make -f xx指定makefile
  • 任何在 makefile 中的目标都可以被指定成终极目标,但是除了以 - 打头,或是包含了 = 的目标
  • 常用的隐含规则
    • 编译 C 程序的隐含规则。
      .o 的目标的依赖目标会自动推导为 .c ,并且其生成命令是 $(CC) –c (CFLAGS)
    • 编译 C++ 程序的隐含规则。
      .o 的目标的依赖目标会自动推导为 .cc 或是 .C ,并且其生成命令是 (CPPFLAGS) $(CXXFLAGS) 。(建议使用 .cc 作为 C++ 源文件的后缀,而不是 .C )
    • 编译 Pascal 程序的隐含规则。
      .o 的目标的依赖目标会自动推导为 .p ,并且其生成命令是 $(PC) –c $(PFLAGS) 。
    • 汇编和汇编预处理的隐含规则。
      .o 的目标的依赖目标会自动推导为 .s ,默认使用编译器 as ,并且其生成命令是: $ (AS)
      $(ASFLAGS) 。 .s 的目标的依赖目标会自动推导为 .S ,默认使用 C 预编译器 cpp ,并且
      其生成命令是: $(AS) $(ASFLAGS) 。
    • 链接 Object 文件的隐含规则。
      目标依赖于 .o ,通过运行 C 的编译器来运行链接程序生成(一般是 ld ),其生成命令
      是: $(CC) $(LDFL
  • AR : 函数库打包程序。默认命令是 ar
    AS : 汇编语言编译程序。默认命令是 as
    CC : C 语言编译程序。默认命令是 cc
    CXX : C++ 语言编译程序。默认命令是 g++
    CO : 从 RCS 文件中扩展文件程序。默认命令是 co
    CPP : C 程序的预处理器(输出是标准输出设备)。默认命令是 $(CC) –E
  • 所谓自动化变量,就是这种变量会把模式中所定义的一系列的文件自动地挨个取出,直至所有的符合模式的文件都取完了。
  • $@ : 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么, $@ 就是匹配于目标中模式定义的集合。
    $% : 仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是 foo.a(bar.o),那么,$% 就是 bar.o , $@ 就是 foo.a 。如果目标不是函数库文件(Unix 下是 .a , Windows下是 .lib ),那么,其值为空。
    $< : 依赖目标中的第一个目标名字。如果依赖目标是以模式(即 % )定义的,那么 $< 将是符合模式的一系列的文件集。注意,其是一个一个取出来的。
    $? : 所有比目标新的依赖目标的集合。以空格分隔。
    $^ : 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那么这个变量会去除重复的依赖目标,只保留一份。
    $+ : 这个变量很像 $^ ,也是所有依赖目标的集合。只是它不去除重复的依赖目标。
  • $* : 这个变量表示目标模式中 % 及其之前的部分。如果目标是 dir/a.foo.b ,并且目标的模式是a.%.b ,那么, $* 的值就是 dir/foo 。这个变量对于构造有关联的文件名是比较有效。如果目标中没有模式的定义,那么 $* 也就不能被推导出,但是,如果目标文件的后缀是 make 所识别的,那么 $* 就是除了后缀的那一部分。例如:如果目标是 foo.c ,因为 .c 是 make 所能识别的后缀名,所以, $* 的值就是 foo 。这个特性是 GNU make 的,很有可能不兼容于其它版本的 make,所以,你应该尽量避免使用 $* ,除非是在隐含规则或是静态模式中。如果目标中的后缀是 make 所不能识别的,那么 $* 就是空值。

UNIX环境高级编程

  • 内核使用exec函数将程序读入内存,并执行程序
  • 信号用于通知进程发生了某种情况
  • 时钟时间又称墙上时钟时间,它是进程运行的时间总量,其值与系统中同时运行的进程数有关
  • 用户CPU时间是执行用户指令所用的时间量,系统CPU时间是为该进程执行内核程序所经历的时间
  • UNIX系统调用中处理存储空间分配的是sbrk(2),不是一个通用的存储器管理器,它按指定字节数增加或减少进程地址空间
  • UNIX系统shell把文件描述符0与进程的标准输入关联,文件描述符1与标准输出关联,文件描述符2与标准错误关联
  • 文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞
  • 大多数文件系统为改善性能都使用某种预读技术,当检测到正进行顺序读取时,系统就会试图读入比应用所要求的更多数据,并假象应用很快就会读这些数据
  • umask()函数为进程设置文件模式创建屏蔽字
  • chmod,fchmod和fchmodat这三个函数可以更改现有文件的访问权限
  • 黏着位:S_ISVTX位。如果一个可执行程序文件的这一位被设置了,那么当该程序第一次被执行,在其终止时,程序正文部分的一个副本仍被保存在交换区,这使得下次执行该程序时能较快的将其载入内存。后来的UNIX版本称它为保存正文位
  • 全缓冲:在填满标准IO缓冲区后才进行实际IO操作
  • 行缓冲:在输入和输出遇到换行符时,标准IO库进行IO操作
  • 标注错误是不带缓冲的,打开至终端设备的流是行缓冲的,其他流是全缓冲的
  • int main(int argc,char *argv[]),argc是命令行参数的数目,argv是指向参数的各个指针所构成的数组
  • 每个程序都接收到一张环境表,与参数表一样,环境表也是一个字符串指针数组
  • setjmp和longjmp函数能够实现跨越函数的跳跃,这对于处理发生在很深嵌套函数调用中的出错情况是非常有用的
  • ID为0的进程通常是调度进程,常常被称为交换进程。该进程是内核的一部分
  • 进程ID为1通常是init进程,在自举过程结束时由内核调用。此进程负责在自举内核后启动UNIX系统,将系统引导到一个状态。init进程不会终止,它是一个普通的用户进程
  • 进程ID为2的是守护进程(某些UNIX),负责支持虚拟存储器系统的分页操作
  • 子进程是父进程的副本,子进程获得父进程数据空间、堆和栈的副本。这是子进程所拥有的副本,父进程和子进程并不共享这些存储空间部分
  • 一个已经终止、但是其父进程尚未对其进行善后处理的进程被称为僵死进程
  • 取得进程终止状态的函数——waitid()
  • 用fork函数创建新的子进程之后,子进程往往需要调用一种exec函数以执行另一个程序
  • 因为调用exec并不创建新进程,所以前后的进程ID并没有改变,exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段
  • 关于谁能更改ID有若干规则:若进程具有超级用户ID,则setuid函数将实际用户ID,有效用户ID以及保存的设置用户ID设置为uid;若进程没有超级用户特权,但是uid等于实际用户ID或者保存的设置用户ID,则setuid只将有效用户ID设置为uid,而不更改实际用户ID和保存的设置用户ID
  • 一个非特权用户总能交换实际用户ID和有效用户ID
  • 系统通常记录用户登录时使用的名字,用getlogin函数可以获取该登录名
  • 进程可以通过调整nice值选择以更低优先级运行(通过调整nice值降低它对CPU的占有,因此该进程是“有好的”)。只有特权进程允许提高调度权限
  • 进程可以通过nice函数获取或者更改它的nice值
  • 通过串行终端登录至系统和经由网络登录至系统两者之间的主要区别是:网络登陆时,在终端和计算机之间的连接不再是点到点的。在网络登录情况下,login仅仅是一种可用的服务,这与其他网络服务(FTP,SMTP)的性质相同
  • 会话是一个或者多个进程组的集合
  • tcgetpgrp()返回前台进程组ID,它与在fd上打开的终端相关联

 

 


































 

猜你喜欢

转载自blog.csdn.net/weixin_45930223/article/details/128983810