面试问题整理之操作系统和代码相关问题

本文为操作系统以及C/C++相关面试问题整理。
1.引用和多态的区别?

引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。


2.内存分配

1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其
操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回
收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的
全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另
一块区域。 - 程序结束后由系统释放。
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。


3.堆和栈的区别?

管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
空间大小:一般来讲在 32位系统下,堆内存可以达到4G的空间。但是对于栈来讲,一般都是有一定的空间大小的。
碎片问题:对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题。
生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由malloc函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
分配效率:栈的效率比较高。堆的效率比栈要低得多。


4 面向对象的三个特征,分别有什么作用?

三个特征:封装、继承、多态。
1、封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
2、继承:让某个类型的对象获得另一个类型的对象的属性的方法。
3、多态:一个类实例的相同方法在不同情形有不同表现形式。


5.虚函数的作用和实现机制?

作用: 实现了多态的机制。

实现机制: 为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组称为虚函数表(virtual function table, vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针。如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中


6.sizeof在计算变量所占空间大小时采取的机制?

a.对基本数据类型的变量,取得的是数据类型的大小。
b.对结构体类型的变量,取得的是结构体变量所占空间的大小。这其中涉及到字节对齐的问题。
c.对数组类型的变量,取得的是数组的大小,并不是数组元素的个数。
d.对指针类型(包括函数指针)的变量,取得的是指针的大小,即4个字节。
e.对数组形参,与指针相同,返回的是指针的大小,因为c语言中并不传递数组的每个元素值,只是将数组的首地址传给函数。


7.结构体struct和联合体union的区别?

a. struct和union都是由多个不同的数据类型成员组成,但在任何同一时刻,union中只存放了一个被选中的成员, 而struct的所有成员都存在。在struct中,各成员都占有自己的内存空间,它们是同时存在的。一个struct变量的总长度等于所有成员长度之和。在Union中,所有成员不能同时占用它的内存空间,它们不能同时存在。Union变量的长度等于最长的成员的长度。
b. 对于union的不同成员赋值,将会对其它成员重写,原来成员的值就不存在了,而对于struct的不同成员赋值是互不影响的。


8.重载和覆盖的区别是什么?

a. 重载要求函数名相同,但是参数列表必须不同,返回值可以相同也可以不同。
覆盖要求函数名、参数列表、返回值必须相同。
b. 在类中重载是同一个类中不同成员函数之间的关系。
在类中覆盖则是子类和基类之间不同成员函数之间的关系
c. 重载函数的调用是根据参数列表来决定调用哪一个函数。
覆盖函数的调用是根据对象类型的不同决定调用哪一个
d. 在类中对成员函数重载是不能够实现多态。
在子类中对基类虚函数的覆盖可以实现多态


9.如果是自己为一个类写一个sizeof函数,应该考虑哪些问题?

#pragma pack(1)

class Empty{
};   // 1

class A {
public:
    A() {a = 3;}
    int a;
    virtual int funa(){}
};

class A1 {
    int a1;
    virtual int funa(){}
};

class A2 {
    int a2;
    virtual int funa();
};

class Derive1: public A, public A1, public A2 {
    virtual int derive();
};    // 36

class Derive2: public A, public A1, public A2 {
    int der;
    virtual int derive();
};    // 40

class B {
    int b;
};   // 4

class C : public B {
    int c;
    virtual int func();
};   // 16


class D : public A {
    int d;
    virtual int fund();
};   // 16

class E : virtual public A {
public:
    E() {e = 4;}
    int  e;
    virtual int fune() {}
};   // 24

class F : virtual public A, virtual public A1, virtual public B {
    int f;
    virtual int funf(){}
};   // 40

class G : virtual public A, virtual public A1, virtual public A2 {
    virtual int fung(){}
};   // 44


class H : virtual public B {
    int h;
    virtual int funh();
};    // 16

class J {
    //int j;  // 若增加该语句,sizeof(I) 为24
    virtual int funj();
};    //8 

class I : virtual public J {
    int i;
    virtual int funi();
};   // 12

a.空类:1
b.非虚继承
- 父类A无虚函数,子类B有虚函数:sizeof(B) = A数据 + B数据 + sizeof(vptr)
- 父类A有虚函数,子类B有虚函数: sizeof(B) = A数据 + B数据 + sizeof(vptr)
- 父类A、B、C均有虚函数,子类D有虚函数: sizeof(D) = A数据 + B数据 + C数据 + D数据 + 3*sizeof(vptr) (D的虚函数增加在继承的第一个类A的虚函数表里)

c.虚继承
- 父类A无虚函数,子类B有虚函数:sizeof(B) = A数据 + B数据 + sizeof(vptr)
- 父类A有虚函数,子类B有虚函数: sizeof(B) = A数据 + B数据 + 2*sizeof(vptr)
- 父类A、B、C均有虚函数,子类D有虚函数: sizeof(D) = A数据 + B数据 + C数据 + D数据 + 4*sizeof(vptr)
- 父类A有虚函数,但无数据,子类B有虚函数:sizeof(B) = B数据 + sizeof(vptr)

10.虚函数和虚继承对于一个类求sizeof的影响有什么差别?

同上题

11.求最大子串和,说思路。

最大子串和,即求一个数列中和最大的子列,采用动态规划可以有最优算法,时间复杂度为O(N)。因为最大连续子序列和只可能是以位置0~n-1中某个位置结尾。当遍历到第i个元素时,判断在它前面的连续子序列和是否大于0,如果大于0,则以位置i结尾的最大连续子序列和为元素i和前门的连续子序列和相加;否则,则以位置i结尾的最大连续子序列和为元素i。

int maxsequence3(int a[], int len)  
{  
    int maxsum, maxhere;  
    maxsum = maxhere = a[0];   //初始化最大和为a【0】  
    for (int i=1; i<len; i++) {  
        if (maxhere <= 0)  
            maxhere = a[i];  //如果前面位置最大连续子序列和小于等于0,则以当前位置i结尾的最大连续子序列和为a[i]  
        else  
            maxhere += a[i]; //如果前面位置最大连续子序列和大于0,则以当前位置i结尾的最大连续子序列和为它们两者之和  
        if (maxhere > maxsum) {  
            maxsum = maxhere;  //更新最大连续子序列和  
        }  
    }  
    return maxsum;  
} 

12.手写代码,实现一个双向循环链表的增删查操作?

struct list_head {
    struct list_head *next, *prev;
};

/**
 * 添加链表表项
 * insert a new entry between two known consecutive entries.
 *
 * This is only for internal list manipulation where we konw
 * the prev/next entries already
 */
static inline void __list_add(struct list_head *new,
                 struct list_head *prev,
                 struct list_head *next)
{
    next->prev = new;
    new->next = next;
    new->prev = prev;
    prev->next = new;
}

/**
 * 添加新的链表项
 * list_add    -     add a new entry
 * @new: new entry to be added
 * @head: list head to add it after
 *
 * Insert a new entry after the specified head.
 * This is good for implenting stacks.
 */ 
static inline void list_add(struct list_head *new, struct list_head *head)
{
    __list_add(new, head, head->next);
}

/**
 * 在尾部添加新链表项
 * list_add_tail    -     add a new entry
 * @new: new entry to be added
 * @head: list head to add it before
 *
 * Insert a new entry before the specified head.
 * This is good for implenting queue.
 */ 
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
    __list_add(new, head->prev, head);
}

/*
 * Delete a list entry by making the prev/next entries
 * point to each other
 *
 * This is only for internal list manipulation where we know
 * the prev/next entries already!
 */
 static inline void __list_del(struct list_head *prev, struct list_head *next)
 {
    next->prev = prev;
    prev->next = next;
 }

/**
 * list_del    -    delete entry from list
 * @entry: the element to delete from the list
 * Note: list_empty on entry does not return true after this, the entry is
 * in an undefined state.
 */
static inline void list_del(struct list_head *entry)
{
    __list_del(entry->prev, entry->next);
    entry->next = LIST_POSITION1;
    entry->prev = LIST_POSITION2;
}

13.模板类的偏特化?

模板分为类模板与函数模板,特化分为全特化与偏特化。全特化就是限定死模板实现的具体类型,偏特化就是如果这个模板有多个类型,那么只限定其中的一部分。对于函数模板,却只有全特化,不能偏特化。


14.两个数相乘,小数点后位数没有限制,请写一个高精度算法。

算法提示:
输入 string a, string b; 计算string c=a*b; 返回 c;
1,纪录小数点在a,b中的位置l1,l2,则需要小数点后移动位置数为l=length(a)+length(b)-l1-l2-2;
2, 去掉a,b中的小数点,(a,b小数点后移,使a,b变为整数)
3, 计算c=a*b; (同整数的大数相乘算法)
4,输出c,(注意在输出倒数第l个数时,输出一个小数点。若是输出的数少于l个,就补0)


15.找出1-10w中没有出现的两个数字

方法1:
设这些数保存在数组A中,用一个10w的数组B标志某一个数是否出现,i出现则B[i]=1,没出现则B[i]=0;扫描数组B,查找缺失的两个数。
时间复杂度:O(N)
空间复杂度:O(N)

方法2:
设确实的两个数为X和Y,则我们可以通过累加1到10W的和 减去 数组A的和,得到X+Y的值S1:
X+Y = S1
同理,我们可以得到1到10W的平方和 减去 数组A中每个数的平方和,从而得到X^2 + Y^2的值:
X^2 + Y^2 = S2
根据两个方程可以解出X和Y。
这里需要注意的是,计算平方和的时候有可能会越界,可以使用unsigned long long,或者一边加(1到10W的平方和)一边减(数组A中每个数)。


16.判断数字是否出现在40亿个数中?给40亿个不重复的unsignedint的整数,没排过序的,然后再给几个数,如何快速判断这几个数是否在那40亿个数当中?

unsigned int 的取值范围是0到2^32-1。我们可以申请连续的2^32/8=512M的内存,用每一个bit对应一个unsigned int数字。首先将512M内存都初始化为0,然后每处理一个数字就将其对应的bit设置为1。当需要查询时,直接找到对应bit,看其值是0还是1即可。

将这40亿个数分成两类: 最高位为0、最高位为1
并将这两类分别写入到两个文件中,其中一个文件中数的个数<=20亿,而另一个>=20亿(这相当于折半了);与要查找的数的最高位比较并接着进入相应的文件再查找。
再然后把这个文件为又分成两类: 次最高位为0、次最高位为1
并将这两类分别写入到两个文件中,其中一个文件中数的个数<=10亿,而另一个>=10亿(这相当于折半了);> 与要查找的数的次最高位比较并接着进入相应的文件再查找
…….
以此类推,就可以找到了,而且时间复杂度为O(logn)

17.两个整数集合A和B,求其交集两个整数集合A和B,求其交集。


18.从10G个数中找到中数在一个文件中有10G个整数,乱序排列,要求找出中位数。内存限制为2G。

假设整数用32bit来表示。
第一步:要表示10G个整数,最少需要一个64位的数据空间。(10G = 5 * 2^31 > 2^32 )
第二步:分区间
2G的内存,能够表示多少个64bit,就能分多少个区间。(一个区间 就表示 一个64bit的数据空间)
区间数位:2G / 64bit = 256M 个区间。
第三步:求区间表示范围
32bit的整数最大值为2^32-1,所以区间的范围是2^32 / 256M = 16.
即0 ~ 15 ,16 ~ 31,32 ~ 47,……(总共256M个)
此时我们有 256M个区间,大小总共为256M * 64bit = 2G内存。
第四步:遍历10G个整数。每读取一个整数就将此整数对应的区间+1。
第五步:找出中位数所在的区间
统计每个区间中整数的值。然后从第一个区间的整数值开始累加。当累加到5G时,停止。此时的区间便包含中位数。记下此区间所表示的范围,设为[a,a+15].并且记下此区间之前所有区间的累加和,设为m。释放掉除包含中位数区间的其他所有区间的内存。
第六步:再次遍历10G个整数,统计出现在区间[a,a+15]中每个值的计数,有16个数值,按照a到a+15排序。设为n0,n1,n2,…n15
第七步:当m+n0+n1+…+nx首次大于5G时,此时的 a+x 就是所求的中位数。


19.统计论坛在线人数分布。求一个论坛的在线人数,假设有一个论坛,其注册ID有两亿个,每个ID从登陆到退出会向一个日志文件中记下登陆时间和退出时间,要求写一个算法统计一天中论坛的用户在线分布,取样粒度为秒。

一天总共有 3600*24 = 86400秒。
定义一个长度为86400的整数数组int delta[86400],每个整数对应这一秒的人数变化值,可能为正也可能为负。开始时将数组元素都初始化为0。
然后依次读入每个用户的登录时间和退出时间,将与登录时间对应的整数值加1,将与退出时间对应的整数值减1。
这样处理一遍后数组中存储了每秒中的人数变化情况。
定义另外一个长度为86400的整数数组int online_num[86400],每个整数对应这一秒的论坛在线人数。
假设一天开始时论坛在线人数为0,则第1秒的人数online_num[0] = delta[0]。第n+1秒的人数online_num[n] = online_num[n-1] + delta[n]。
这样我们就获得了一天中任意时间的在线人数。

另外,如果只知道IP,为了方便查找,可以采用哈希表进行记录,较数组更为方便。

20.需要多少只小白鼠才能在24小时内找到毒药有1000瓶水,其中有一瓶有毒,小白鼠只要尝一点带毒的水24小时后就会死亡,至少要多少只小白鼠才能在24小时时鉴别出那瓶水有毒?

每个老鼠只有死或活2种状态,因此每个老鼠可以看作一个bit,取0或1
N个老鼠可以看作N个bit,可以表达2^N种状态(其中第i个状态代表第i个瓶子有毒)
例如:当N=2时,可以表达4种状态
0,0( 一号老鼠活,二号老鼠活)
0,1( 一号老鼠活,二号老鼠死)
1,0( 一号老鼠死,二号老鼠活)
1,1( 一号老鼠死,二号老鼠死)
具体来说,有A、B、C、D这4个瓶子,一号老鼠喝A和B, 二号老鼠喝B和C
如果 0,0 ( 一号老鼠活,二号老鼠活),说明是D有毒,第0个状态代表第4个瓶子有毒
如果 0,1 ( 一号老鼠活,二号老鼠死) ,说明是C有毒 ,第1个状态代表第3个瓶子有毒
如果 1,0 ( 一号老鼠死,二号老鼠活) ,说明是A有毒 ,第2个状态代表第1个瓶子有毒
如果 1,1 ( 一号老鼠死,二号老鼠死) ,说明是B有毒 ,第3个状态代表第2个瓶子有毒


21.写string类的构造,析构,拷贝函数。

class String {
public:
    String() {  // 默认构造函数
        len = 0;
        str = new char[len + 1];
        str[0] = '\0';
    }

    String(const char* s) {  // 构造函数
        len = strlen(s);
        str = new char[len + 1];
        strcpy(str, s);
    }

    String(const String& s) {  // 赋值构造函数
        len = s.len;
        str = new char[len + 1];
        strcpy(str, s.str);
    }

    ~String() {   // 析构函数
        delete[] str;
    }

    String& operator=(const String& s) {  // 拷贝函数
        if (this == &s) return *this;
        delete[] str;
        len = s.len;
        str = new char[len + 1];
        strcpy(str, s.str);
        return *this;
    }
    // 以上拷贝函数先delete在分配内存,如果内存分配失败将导致程序奔溃,可做以下修改:
    String& operator=(const String& s) {
        if (this != &s) {
            String strTemp(s);  // 调用复制构造函数,先分配好内存
            char* p = strTemp.str;
            strTemp.str = str;  // 指向原本就该delete的this->str,函数结束时strTemp会被析构,str指向的空间也会被释放
            str = p;
        }
        return *this;
    }

    String& operator=(const char* s) {  // 拷贝函数
        delete[] str;
        len = strlen(s);
        str = new char[len + 1];
        strcpy(str, s);
        return *this;
    }
private:
    char* str;
    int len;
};

22.宏定义和展开(必须精通)

#include <stdio.h>
#define f(a,b) a##b
#define g(a)  #a
#define h(a) g(a)
int main()
{
  printf("%s/n",h(f(1,2)));   // 12
  printf("%s/n",g(f(1,2)));   // f(1,2)
  return 0;
}

# 将右边的参数做整体的字符串替换。
对于#的参数,即便是另一个宏,也不展开,仍然作为字符串字面信息输出。所以,g(f(1,2)) 为 f(1,2)。

对于h(f(1,2)),由于h(a)是非#或##的普通宏,需要先宏展开其参数a,即展开f(1,2)为12,则h(a) 宏替换为h(12),进而宏替换为g(12), 进而宏替换为12

## 将左右两边的参数做整体的字符串拼接替换,则f(1,2)为12。
同#,对于##的参数,即便是另一个宏,也不展开,仍然作为字符串字面信息输出。此外,有一个限制是,经过##替换后的内容必须能够作为一个合法的变量。


23.哪些库函数属于高危函数,为什么?(strcpy等等)。

标准库中的许多字符串处理和IO流读取函数是导致缓冲区溢出的罪魁祸首。高危函数

#include <string.h>

char *strcpy( char *to, const char *from );

char * strncpy(char *dest, char *src, size_t n); 

errno_t __cdecl strcpy_s(char*_Destination,rsize_t _SizeInBytes,char const* _Source);

strcpy() 函数将源字符串复制到缓冲区。没有指定要复制字符的具体数目!如果源字符串碰巧来自用户输入,且没有专门限制其大小,则有可能会造成缓冲区溢出!
也可以使用strncpy() 来完成同样的目的。

而strcpy_s版本之所以安全,是因为他们在接口增加了一个参数numElems来表明dest中的字节数,防止目标指针dest中的空间不够而导致出现Bug,同时返回值改成返回错误代码,而不是为了一些所谓的方便而返回char*。这样接口的定义就比原来安全很多。

void * memcpy( void *dst, const void *src, size_t len);

当len超过dst实际内存大小时,存在踩越界;

char* strcpy(char* dest,const char* src)

当src字符串长度超过dest字符串长度,存在踩越界;

char * strncpy( void *dst, const void *src, size_t len);

当len超过dst实际内存大小时,存在踩越界;
当len小于src实际内存大小时,实际按len大小拷贝到dest中,不会置结束符,可能会存在访问越界风险。

int sprintf(char * buf, const char *fmt, …)

当参数fmt变长字符串长度超过buf长度,存在踩越界;

char* strcat(char* dest,const char* src)

当src字符串长度超过dest剩余长度,存在踩越界;

char * strncat(char *dest, const char *src, size_t count)

当count超过dest剩余内存大小时,存在踩越界;


24.指针和引用的区别?

a. 指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已;
b. 可以有const指针,但是没有const引用;
c. 指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)
d. 指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;
e. 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。
f. “sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;
g. 指针和引用的自增(++)运算意义不一样;

对实参进行修改,用指针传递参数,是对指针指向的内存单元中的数据进行操作;
用引用作为函数参数传递时,实质上传递的是实参本身,而非拷贝,不仅节约时间,而且可以节约空间。


25.extern C的作用?

  1. C++语言在编译的时候为了解决函数的重载问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。
  2. 当extern在头文件中: extern int g_Int; 它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块活其他模块中使用,它是一个声明不是定义!也就是说B模块(编译单元)要是引用模块(编译单元)A中定义的全局变量或函数时,它只要包含A模块的头文件即可,在编译阶段,模块B虽然找不到该函数或变量,但它不会报错,它会在连接时从模块A生成的目标代码中找到此函数。 只在头文件中做声明!一般定义static全局变量时,都把它放在原文件中而不是头文件!

26.volatile的作用?

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化(优化编译器把变量从内存装入CPU寄存器中),从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用volatile声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。

一般说来,volatile用在如下的几个地方:
1) 中断服务程序中修改的供其它程序检测的变量需要加volatile;
2) 多任务环境下各任务间共享的标志应该加volatile;
3) 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;


27.多重类构造和析构的顺序?

构造类时,会先构造其父类,然后创建类成员,最后调用本身的构造函数。
析构函数的调用顺序与构造函数相反。


28.static const的用法?

static变量的作用:

文件中的static变量只能在该文件中访问;

static变量在程序结束后才释放;

非static变量在全局变量中是会进行初始化的,但是在局部变量中,比如函数体内部是不会初始化的。但是static变量会在所有情况下进行初始化。

类中static的作用:

static 数据成员是属于整个类的,不属于某个对象,必须在内定义的外部进行定义,在类定义体中定义是错误的。

静态成员函数不能访问非静态函数或者变量。static函数中不能使用this关键字,不能声明为virtual虚函数。

const

const int y = 10;  // const常量

int x = 30;
const int* p1 = &x;  // 指向常量的指针,指针可以指向其他常量,但不能通过指针修改数据
int* const p2 = &x;  // 指向变量的常量指针,指针不可以指向其他常量,但能通过指针修改数据
const int* const p3 = &x;  // 指向常量的常量指针。

类中的const:

class C {
    ...
    const C& fun(const C& c) const {
        ...
    }
    ...

    static int c;   // 静态变量
    const int b;    // 常量

    const static int a = 100;
    static const int b = 200;
    ...
};

int C::c = 10;  // 静态变量的初始化

第一个const修饰函数返回值,返回一个const值,不能被修改;

第二个const修饰函数参数,函数中不能修改该参数,可以接收const或者非const的形参;

第三个const修饰调用对象,也就是this,保证调用对象不会被修改。

静态变量c不能在类声明中初始化,只能在类外初始化;

常量d只能在构造函数中初始化。

static const和 const static一样,也可用枚举法表示(enum {a = 100};)


29.malloc/free和new/delete的区别?

  1. malloc/free是C/C++语言的标准库函数,new/delete是C++的运算符。
  2. new自动计算需要分配的空间,而malloc需要手工计算字节数。
  3. new将调用constructor,而malloc不能;delete将调用destructor,而free不能。
  4. new可以重载, malloc不能。
  5. new返回指定类型的指针,而malloc返回void*,需要进行强制转换。

30.printf的可变参数是怎么实现的,如果参数个数不匹配会发生什么,比如字符串需要3个参数,但是只传了2个或者4个分别会发生什么

see this blog


31.C语言各种指针

void* 类型指针:通用变体类型指针;可以不经转换,赋给其他指针,函数指针除外;malloc返回的就是void*类型。

NULL指针:是一个标准规定的宏定义;#define NULL ((void *) 0) 用来表示空指针常量;

零指针:指针值为0,零值指针,没有存储任何内存地址的指针;可以使任意一种指针类型,eg:void * ;int * ;double *;

空指针:指针赋值为0;0*7;3-3等之后,指针即变成空指针;即:空指针不指向任何实际的对象或者函数;NULL指针和零指针都是空指针。

野指针:指向垃圾内存的指针;(1)指针变量没有初始化(2)指针被delete或者free之后没有置为空(3)指针操作超越了变量的范围

悬垂指针:指向曾经存放对象的内存,但是该对象已经不存在了;delete操作完成后的指针就是悬垂指针,此时需要将指针置为0变为零值指针;


32.函数调用栈里面存储的是什么
see this blog


33.malloc的返回值,还有其他内存分配函数吗,有什么区别

malloc函数可以从堆上获得指定字节的内存空间,其函数原型如下:

void * malloc(int n)

其中,形参n为要求分配的字节数。如果函数执行成功,malloc返回获得内存空间的首地址;如果函数执行失败,那么返回值为NULL。由于malloc函数值的类型为void型指针,因此,可以将其值类型转换后赋给任意类型指针,这样就可以通过操作该类型指针来操作从堆上获得的内存空间。

需要注意的是,malloc函数分配得到的内存空间是未初始化的。因此,一般在使用该内存空间时,要调用另一个函数memset来将其初始化为全0。memset函数的声明如下:
void * memset (void * p,int c,int n)

void *calloc(int n,int size)

函数返回值为void型指针。如果执行成功,函数从堆上获得size X n的字节空间,并返回该空间的首地址。如果执行失败,函数返回NULL。该函数与malloc函数的一个显著不同时是,calloc函数得到的内存空间是经过初始化的,其内容全为0。

可以实现内存分配和内存释放的功能,其函数声明如下:

void * realloc(void * p,int n)

其中,指针p必须为指向堆内存空间的指针,即由malloc函数、calloc函数或realloc函数分配空间的指针。realloc函数将指针p指向的内存块的大小改变为n字节。如果n小于或等于p之前指向的空间大小,那么。保持原有状态不变。如果n大于原来p之前指向的空间大小,那么,系统将重新为p从堆上分配一块大小为n的内存空间,同时,将原来指向空间的内容依次复制到新的内存空间上,p之前指向的空间被释放。relloc函数分配的空间也是未初始化的。


34.free怎么释放


35.为什么构造函数不能为虚?

vptr指针指向虚函数表,执行虚函数的时候,会调用vptr指针变量指向的虚函数表中的虚函数。
当定义个对象的时候,首先会分配内存空间,然后再执行构造函数对该对象进行初始化构造。vptr变量是在构造函数中进行初始化的。又因为要想执行虚函数必须通过vptr变量找到虚函数表(在构造函数初始化vptr变量之前是不会调用虚函数的)。
所以不能将构造函数声明为虚函数。


36.析构函数为什么必须是虚的?

原因是基类指针指向了派生类对象,而基类中的析构函数却是非virtual的,虚函数是动态绑定的基础。现在析构函数不是virtual的,因此不会发生动态绑定,而是静态绑定,指针的静态类型为基类指针,因此在delete时候只会调用基类的析构函数,而不会调用派生类的析构函数。这样,在派生类中申请的资源就不会得到释放,就会造成内存泄漏。


37.虚函数表是什么时期创建的?

虚函数表中的virtual functions地址是如何被建构起来的?在C++中,virtual functions(可经由其class object被调用)可以在编译时期获知。此外,这一组地址是固定不变的,执行期不可能新增或替换之。由于程序执行时,表格的大小和内容都不会改变,所以其建构和存取皆可以由编译器完全掌控,不需要执行期的任何介入。


38.map用[]和find查找有什么区别?

map的下标运算符[]的作用是:将关键码作为下标去执行查找,并返回对应的值;如果不存在这个关键码,就将一个具有该关键码和值类型的默认值的项插入这个map。
map的find函数:用关键码执行查找,找到了返回该位置的迭代器;如果不存在这个关键码,就返回尾迭代器。


39.map对key的要求?

1 支持拷贝构造
2 支持operator=
3 operator< 如果没有operator<那么 map模板必须增加第三个模板参数
4 默认的构造函数


40.仿函数

也就是函数对象,行为类似函数的对象,其类定义中必须重载function call运算符operator()。
- 生成器(generator)是不用参数就可以调用的函数符(仿函数);
- 一元函数(unary function) 是用一个参数就可以调用的函数符;
- 二元函数(binary function)是用两个参数就可以调用的函数符;
- 当然,这些概念都有相应的改进版:
- 返回bool值的一元函数是谓词(predicate);
- 返回bool值的二元函数是二元谓词(binary predicate);
- 记录了参数、返回值类型的仿函数称之为自适应的仿函数;

函数适配器:
(1)绑定器(binder),是一种函数适配器,它通过将一个操作数绑定到给定值而将二元函数对象转换为一元函数对象。常用绑定器有两个:bind1st, bind2nd,分别是把值绑定到二元函数的第一个参数或者是第二个参数。
(2)求反器(negator),是一种函数适配器,它将谓词函数对象的真值求反。

成员函数适配器是一个一元函数,参数为调用这个成员函数的对象(或者对象指针)。当然如果是对象的话适配器就是mem_fun_ref,如果是指针的话就是mem_fun.


41.使用指针,怎么尽量避免segment fault


42.vector和链表的区别

(1)vector为存储的对象分配一块连续的地址空间,随机访问效率很高。但是插入和删除需要移动大量的数据,效率较低。尤其当vector中存储的对象较大,或者构造函数复杂,则在对现有的元素进行拷贝的时候会执行拷贝构造函数。
(2)list中的对象是离散的,随机访问需要遍历整个链表,访问效率比vector低。但是在list中插入元素,尤其在首尾插入,效率很高,只需要改变元素的指针。
(3)vector是单向的,而list是双向的;
(4)vector中的iterator在使用后就释放了,但是链表list不同,它的迭代器在使用后还可以继续用。

使用原则:
(1)如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;
(2)如果需要大量高效的删除插入,而不在乎存取时间,则使用list;
(3)如果需要高效的随机存取,还要大量的首尾的插入删除则建议使用deque,它是list和vector的折中;


43.struct和class的区别

对于成员访问权限以及继承方式,class中默认的是private的,而struct中则是public的。class还可以用于表示模板类型,struct则不行。


44.操作系统执行可执行程序时,内存分配是怎样的?

1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap)— 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3、全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区(text)—存放函数体的二进制代码。


45.线程和进程的区别?

  • 进程是资源分配的最小单位,线程是程序执行的最小单位。
  • 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
  • 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
  • 但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

46.共享内存的使用实现原理(必考必问,共享内存段被映射进进程空间之后,存在于进程空间的什么位置?共享内存段最大限制是多少?)

mmap函数要求内核创建一个新的虚拟存储器区域,最好是从地址start开始的一个区域,并将文件描述符fd指定对象的一个连续的片(chunk)映射到这个新的区域。
SHMMNI为128,表示系统中最多可以有128个共享内存对象。


47.多进程和多线程的区别?


48.内核级线程与用户级线程

  • 内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。
  • 用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。
  • 用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。
  • 在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。
  • 用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是可以运行在任何状态下的程序。

49.线程同步机制?

(1)原子操作函数、无锁化编程:不使用系统提供的锁,而是直接利用cpu提供的指令,实现互斥操作。在原子操作函数的执行不会被打断或被干涉。线程执行原子函数的过程不会被中断。
(2)互斥锁:互斥锁用于确保同一个时间只有一个线程能访问被互斥锁保护的资源。加锁互斥量的线程和解锁互斥量的线程必须是同一个线程。
(3)读写锁:读写锁有3种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
(4)自旋锁:自旋锁和互斥量类似,但是它不是通过休眠使线程阻塞,而是在获取锁之前一直处理忙等待(自旋)的阻塞状态。自旋锁可用于以下情况:锁被持有的时间短,而且线程并不希望在重新调度上花费太多成本。
(5)条件变量:条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。
(6)屏障:屏障是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,知道所有的合作线程都达到某一点,然后从该点继续执行。它允许任意数量的线程等待,直到所有的线程完成处理工作。

一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞 (blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而Spin lock则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。


50.什么是死锁?如何避免死锁?

死锁就是两个或多个进程被无限期地阻塞、相互等待的一种状态;

产生死锁的必要条件:
  (1)互斥,一个资源每次只能被一个进程使用
  (2)不可抢占,进程已获得的资源,在未使用完之前,不能强行剥夺
  (3)占有并等待,一个进程因请求资源而阻塞时,对已获得的资源保持不放
  (4)环形等待,若干进程之间形成一种首尾相接的循环等待资源关系。

1) 预防死锁。
  破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁。由于所施加的限制条件往往太严格,可能会导致系统资源利用率和系统吞吐量降低。
2) 避免死锁。
  在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。
3) 检测死锁。
  允许系统在运行过程中发生死锁,但可通过系统所设置的检测机构,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源,然后采取适当措施,从系统中将已发生的死锁清除掉。
4) 解除死锁。
  这是与检测死锁相配套的一种措施。当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来。常用的实施方法是撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行。死锁的检测和解除措施,有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度也最大。


51.守护进程?

  • Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。
  • 守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。
  • 一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。
  • 守护进程的名称通常以d结尾,比如sshd、xinetd、crond等

52.Linux的内存管理机制?

  • Linux采用虚拟内存机制和分页机制来管理内存。
    直接从物理内存读写数据要比从硬盘读写数据要快的多,因此,我们希望所有数据的读取和写入都在内存完成,而内存是有限的,所以引入了虚拟内存机制。
    物理内存就是系统硬件提供的内存大小,是真正的内存,相对于物理内存,在linux下还有一个虚拟内存的概念,虚拟内存就是为了满足物理内存的不足而提出的策略,它是利用磁盘空间虚拟出的一块逻辑内存,用作虚拟内存的磁盘空间被称为交换空间(Swap Space)。
    linux会在物理内存不足时会将暂时不用的内存块信息写到交换空间,物理内存得到了释放,这块内存就可以用于其它目的,当需要用到原始的内容时,这些信息会被重新从交换空间读入物理内存。
    Linux的内存管理采取的是分页存取机制,将进程空间放在多个页中。进程运行时不是所有的页都在内存中,而只是一部分而已。每个进程有个页表来指明某个页是否在物理内存中,如果在则写明该页在物理内存中的页框号;否则该页在虚拟内存中,当进程需要用到出狱虚拟内存中的页面时,会产生缺页异常,操作系统会根据一定的页面置换算法将该页面调入物理内存。
    进程包含一个虚拟地址(页号+偏移量),寄存器保存了页表的基地址,页号指出了页表中的页表项,得到该页在物理内存中的页框号,页框号+偏移量就是物理地址。

  • 页面置换算法:
    (1)最佳OPT :选择置换下次访问距当前时间最长的那些页,可以看出这能导致最少的缺页中断,但是由于它要求操作系统必须知道将来的事情,显然是不可实现的。只能作为性能衡量标准。
    (2)最近最少使用LRU :置换内存中上次使用距当前最远的页。根据局部性原理,这也是最不可能访问的页。为每一页添加最后一次访问的时间戳,或者维护一个页的访问栈。性能优异,但两种方法都开销大。
    (3)先进先出FIFO :把分配给进程的页框视为一个循环缓冲区,按循环方式移动页。即选择驻留在内存时间最长的页。但这个推断常常是错的。
    (4)时钟Clock :开销较小但性能接近LRU。为每个页框关联一个附加位。当被访问到时置1。置换的候选页框集合视为一个循环缓冲区,指针扫描缓冲区,选择第一个附加为位0的页框进行置换。当指针划过一个附加位为1的,则置0。可以增加关联的附加位让时钟策略更有效。除了访问的附加位,还可以添加修改的附加位。
    (5)最近次数最少LFU,要求在页置换时置换引用计数最小的


53.Linux的任务调度机制和调度时机

  • linux进程的调度时机大致分为两种情况: 一种是进程自愿调度;另一种是发生强制性调度。 首先,自愿的调度随时都可以进行。在内核空间中,进程可以通过schedule()启动一次调度;在用户空间中,可以通过系统调用pause()达到同样的目的。如果要为自愿的暂停行为加上时间限制,在内核中使用schedule_time(),而在用户空间则使用nanosleep()系统调用。 linux中,强制性的调度发生在每次从系统调用返回的前夕,以及每次中断或异常处理返回用户空间的前夕。应注意的是,从内核态返回到用户态是进程调度发生的必要条件,而不是充分条件,还要取决于当进程task_struct结构中的need_resched是否为1。
    从进程调度的时机可以看出,内核的调度方式为“有条件的剥夺方式”。当进程在用户空间运行,不管自愿不自愿,一旦有必要比如时间片用完,内核就可以暂时剥夺其运行而调度其他进程运行。而进程一旦进入内核空间,即进入核心态时,尽管知道应该要调度了,但实际上却不会发生,一直要到该进程返回到用户空间前夕才能剥夺其运行。
  • Linux任务调度策略
    Linux支持SCHED_FIFO、SCHED_RR和SCHED_OTHER的调度策略。
    linux用函数goodness()统一计算进程(包括普通进程和实时进程)的优先级权值,该权值衡量一个处于可运行状态的进程值得运行的程度,权值越大,进程优先级越高。 每个进程的task_struct结构中,与goodness()计算权值相关的域有以下四项:policy、nice(2.2版内核该项为priority)、counter、rt_priority。其中,policy是进程的调度策略,其可用来区分实时进程和普通进程,实时进程优先于普通进程运行。nice从最初的UNIX沿用而来,表示进程的静态负向优先级,其取值范围为19~-20,以-20优先级最高。counter表示进程剩余的时间片计数值,由于counter在计算goodness()时起重要作用,因此,counter也可以看作是进程的动态优先级。rt_priority是实时进程特有的,表示实时优先级。

54.32位系统一个进程最多多少堆内存?

32位linux不打开PAE,则最多只能识别出4GB内存,若打开PAE,则最多可以识别出64GB内存。但是 32位系统下的进程一次最多只能寻址4GB的空间。


55.死锁的条件及银行家算法

利用银行家算法解决死锁:
1).银行家算法中的数据结构
(1).可利用资源向量Available
(2).最大需求矩阵Max
(3).分配矩阵Allocation
(4).需求矩阵Need
2).银行家算法
Request请求向量,
(1).如果Request[i] <= Need[i][j]转下步,否则它所需要的资源数已超过它所需要的最大值
(2).如果Request[i] <= Available[i][j]转下步,否则尚无足够资源,进程需等待
(3).系统试分配给进程p,并修改Available,Allocation和Need
Available[j] -= Request[j]
Allocation[i][j] += Request[j]
Need[i][j] -= Request[j]
(4)系统执行安全性算法,检查此次资源分配后系统是否处于安全状态.若安全,才正式分配;否则恢复原来的分配状态,让该进程等待
3).安全性算法
(1).设置两个向量,工作向量Work,在执行安全性算法开始时 Work=Available;Finish:表示有足够的资源分配给进程,使之运行完成,Finish[i]=false;当有足够资源分配给进程时,再另Finish[i]=false
(2).从进程集合中找到一个满足该条件的进程:
Finish[i]=false
Need[i][j] <= Work[j]
(3).当进程获得资源后,可顺利执行,并修改Work向量和Finsh向量
Work[i] += Allocation[i][j]
Finish[i]=true
(4).如果所有进程的Finish[i]=true说明系统处于安全状态,否则系统处于不安全状态.


56.linux的五种IO方式(阻塞与非阻塞、同步与异步的理解)

同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,但是当前线程还是激活的。
异步,就是调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
非阻塞调用,不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

阻塞和非阻塞是指当进程访问的数据如果尚未就绪,进程是否需要等待,也就是未就绪时是直接返回还是等待就绪;而同步和异步是指访问数据的机制,同步一般指主动请求并等待I/O操作完毕的方式,当数据就绪后在读写的时候必须阻塞,异步则指主动请求数据后便可以继续处理其它任务,随后等待I/O,操作完毕的通知,这可以使进程在数据读写时也不阻塞。

也就是说阻塞和非阻塞是指数据未到达时要不要等待,而同步异步是指收到通知的方式。

Linux下的五种I/O模型
1) 阻塞I/O:进程会一直阻塞,直到数据拷贝完成
2) 非阻塞I/O:非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);在数据拷贝的过程中,进程是阻塞的;
3) I/O复用(select 和poll):主要是select和epoll;对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性;关键是能实现同时对多个IO端口进行监听。
4) 信号驱动I/O :允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
5) 异步I/O:当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作。


57.进程调度算法

  • 先来先服务 (FCFS,first come first served)
    在所有调度算法中,最简单的是非抢占式的FCFS算法。
    算法原理:进程按照它们请求CPU的顺序使用CPU.就像你买东西去排队,谁第一个排,谁就先被执行,在它执行的过程中,不会中断它。当其他人也想进入内存被执行,就要排队等着,如果在执行过程中出现一些事,他现在不想排队了,下一个排队的就补上。此时如果他又想排队了,只能站到队尾去。
    算法优点:易于理解且实现简单,只需要一个队列(FIFO),且相当公平
    算法缺点:比较有利于长进程,而不利于短进程,有利于CPU 繁忙的进程,而不利于I/O 繁忙的进程
  • 最短作业优先(SJF, Shortest Job First)
    短作业优先(SJF, Shortest Job First)又称为“短进程优先”SPN(Shortest Process Next);这是对FCFS算法的改进,其目标是减少平均周转时间。
    算法原理:对预计执行时间短的进程优先分派处理机。通常后来的短进程不抢先正在执行的进程。
    算法优点:相比FCFS 算法,该算法可改善平均周转时间和平均带权周转时间,缩短进程的等待时间,提高系统的吞吐量。
    算法缺点:对长进程非常不利,可能长时间得不到执行,且未能依据进程的紧迫程度来划分执行的优先级,以及难以准确估计进程的执行时间,从而影响调度性能。
  • 最高响应比优先法(HRRN,Highest Response Ratio Next)
    最高响应比优先法(HRRN,Highest Response Ratio Next)是对FCFS方式和SJF方式的一种综合平衡。FCFS方式只考虑每个作业的等待时间而未考虑执行时间的长短,而SJF方式只考虑执行时间而未考虑等待时间的长短。因此,这两种调度算法在某些极端情况下会带来某些不便。HRN调度策略同时考虑每个作业的等待时间长短和估计需要的执行时间长短,从中选出响应比最高的作业投入执行。这样,即使是长作业,随着它等待时间的增加,W / T也就随着增加,也就有机会获得调度执行。这种算法是介于FCFS和SJF之间的一种折中算法。
    算法原理:响应比R定义如下: R =(W+T)/T = 1+W/T
    其中T为该作业估计需要的执行时间,W为作业在后备状态队列中的等待时间。每当要进行作业调度时,系统计算每个作业的响应比,选择其中R最大者投入执行。
    算法优点:由于长作业也有机会投入运行,在同一时间内处理的作业数显然要少于SJF法,从而采用HRRN方式时其吞吐量将小于采用SJF 法时的吞吐量。
    算法缺点:由于每次调度前要计算响应比,系统开销也要相应增加。
  • 时间片轮转算法(RR,Round-Robin)
    该算法采用剥夺策略。时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法,又称RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
    算法原理:让就绪进程以FCFS 的方式按时间片轮流使用CPU 的调度方式,即将系统中所有的就绪进程按照FCFS 原则,排成一个队列,每次调度时将CPU 分派给队首进程,让其执行一个时间片,时间片的长度从几个ms 到几百ms。在一个时间片结束时,发生时钟中断,调度程序据此暂停当前进程的执行,将其送到就绪队列的末尾,并通过上下文切换执行当前的队首进程,进程可以未使用完一个时间片,就出让CPU(如阻塞)。
    算法优点:时间片轮转调度算法的特点是简单易行、平均响应时间短。
    算法缺点:不利于处理紧急作业。在时间片轮转算法中,时间片的大小对系统性能的影响很大,因此时间片的大小应选择恰当

58.怎样确定时间片的大小:

时间片大小的确定
1.系统对响应时间的要求
2.就绪队列中进程的数目
3.系统的处理能力


59.多级反馈队列(Multilevel Feedback Queue)

多级反馈队列调度算法是一种CPU处理机调度算法,UNIX操作系统采取的便是这种调度算法。
多级反馈队列调度算法描述:
  1、进程在进入待调度的队列等待时,首先进入优先级最高的Q1等待。
  2、首先调度优先级高的队列中的进程。若高优先级中队列中已没有调度的进程,则调度次优先级队列中的进程。例如:Q1,Q2,Q3三个队列,只有在Q1中没有进程等待时才去调度Q2,同理,只有Q1,Q2都为空时才会去调度Q3。
  3、对于同一个队列中的各个进程,按照时间片轮转法调度。比如Q1队列的时间片为N,那么Q1中的作业在经历了N个时间片后若还没有完成,则进入Q2队列等待,若Q2的时间片用完后作业还不能完成,一直进入下一级队列,直至完成。
  4、在低优先级的队列中的进程在运行时,又有新到达的作业,那么在运行完这个时间片后,CPU马上分配给新到达的作业(抢占式)。
  在多级反馈队列调度算法中,如果规定第一个队列的时间片略大于多数人机交互所需之处理时间时,便能够较好的满足各种类型用户的需要。


60.linux中断响应机制

当一个中断发生时,并不是所有的操作都具有相同的急迫性。事实上,把所有的操作都放进中断处理程序本身并不合适。需要时间长的、非重要的操作应该推后,因为当一个中断处理程序正在运行时,相应的IRQ中断线上再发出的信号就会被忽略。另外中断处理程序不能执行任何阻塞过程,如I/O设备操作。因此,Linux把一个中断要执行的操作分为下面的三类:
(1)紧急的(Critical)
这样的操作诸如:中断到来时中断控制器做出应答,对中断控制器或设备控制器重新编程,或者对设备和处理器同时访问的数据结构进行修改。这些操作都是紧急的,应该被很快地执行,也就是说,紧急操作应该在一个中断处理程序内立即执行,而且是在禁用中断的状态下。
(2)非紧急的(Noncritical)
这样的操作如修改那些只有处理器才会访问的数据结构(例如,按下一个键后,读扫描码)。这些操作也要很快地完成,因此,它们由中断处理程序立即执行,但在启用中断的状态下。
(3) 非紧急可延迟的(Noncritical deferrable)
这样的操作如,把一个缓冲区的内容拷贝到一些进程的地址空间(例如,把键盘行缓冲区的内容发送到终端处理程序的进程)。这些操作可能被延迟较长的时间间隔而不影响内核操作,有兴趣的进程会等待需要的数据。
所有的中断处理程序都执行四个基本的操作:
(1) 在内核栈中保存IRQ的值和寄存器的内容。
(2)给与IRQ中断线相连的中断控制器发送一个应答,这将允许在这条中断线上进一步发出中断请求。
(3)执行共享这个IRQ的所有设备的中断服务例程(ISR)。
(4)跳到ret_to_usr( )的地址后终止。


61.exit()与_exit()的区别?

_exit终止调用进程,但不关闭文件,不清除输出缓存,也不调用出口函数。exit函数将终止调用进程。在退出程序之前,所有文件关闭,缓冲输出内容将刷新定义,并调用所有已刷新的“出口函数”(由atexit定义)。

‘exit()’与‘_exit()’有不少区别在使用‘fork()’,特别是‘vfork()’时变得很突出。

‘exit()’与‘_exit()’的基本区别在于前一个调用实施与调用库里用户状态结构(user-mode constructs)有关的清除工作(clean-up),而且调用用户自定义的清除程序

猜你喜欢

转载自blog.csdn.net/u013354486/article/details/80588948