mxxhcm's blog

  • 首页

  • 标签

  • 分类

  • 归档

C++ function overload

发表于 2019-12-06 | 更新于 2019-12-07 | 分类于 C/C++

函数重载

如果同一个作用域内的几个函数名字相同但是形参列表不同,被称为重载函数。main函数不能被重载。

定义重载函数

如下示例,定义了几个重载函数。重载函数的区别在于形参的类型和个数不同。形参可以有名字可以没有名字,也可以有不同的名字,但只要类型和个数相同,就是同一个函数。

1
2
3
4
5
void print(const int i);
void print(const string &s);
void print(const vector<string> &vs);
void print(const set<int> &si);
void print(const map<string, int> &msi);

重载和const形参

顶层const不影响传入函数的对象

  1. 一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
  2. 如果形参是某种类型的指针或引用,即形参是底层const,区分其指向的是常量对象还是非常量对象可以实现函数重载。

const_cast和重载

const_cast在重载函数的情景中有最有用。如下函数:

1
const string& stringCompare(const string &s1, const string &s2);

函数的形参是底层const,可以接收常量或者非常量的实参。但是返回的都是const string的引用,如果输入是两个string的引用,返回一个const string的引用显然是不合理的,这时候就可以使用const_cast了。对上述代码做一个改进:

1
2
3
4
5
6
string& stringCompare(string &s1, string &s2)
{

auto &r = stringCompare(static_cast<const string &>(s1), static_cast<const string &>(s2)a);
return static_cast<string &>(r);
}

调用重载函数

函数匹配是指将函数调用功能和重载函数中的某一个关联起来。编译器根据调用的实参和重载集合中每个函数的形参作比较。调用功能重载函数有三种可能结果:

  1. 找到一个与实参完全匹配的函数,生成调用功能该函数的代码。(包括了类型转换)。为了确定最佳匹配,编译器将实参类型到形参类型的转换划分为了几个等级:
    1. 精确匹配。包括实参和形参类型完全相同;实参从数组类型或者函数类型转化成相应的指针类型;向实参条件顶层const或者从实参中删除顶层const。
    2. 通过const_cast转换实现的匹配。
    3. 通过类型提升实现的匹配。
    4. 通过算术类型转换实现的匹配。
    5. 通过类类型实现的匹配。
  2. 找不到任何一个函数和调用的实参匹配,这个时候编译器发出无匹配的错误
  3. 多于一个函数可以匹配,但是每一个都不是最佳选择,这种错误叫做二义性调用。

重载和作用域

正常来说,将函数声明放在局部作用域内是一个不明智的选择。如果真的这么做了,编译器会首先在内层作用域寻找函数的声明,如果找到了,它会隐藏外部作用域的所有同名声明。

参考文献

1.《C++ Primer》第五版

C/C++ return

发表于 2019-12-06 | 更新于 2019-12-07 | 分类于 C/C++

return和返回值类型

return语句结束当前正在执行的函数并将控制权返回到函数调用的地方。
return语句有两种形式:

1
2
return ;
return expression ;

无返回值函数

没有返回值的语句只能出现在返回值类型是void的函数中。返回void类型的函数不一定非得有return语句,因为这类函数的最后一句会隐式的执行return。

有返回值函数

只要函数的返回值不是return,就一定得有返回值。return语句返回值的类型必须和函数的返回类型相同,或者能隐式的转换成函数的返回类型。

值是如何返回到的

返回一个值的方式和初始化一个变量或者一个形参的方式完全一样:返回的值用于初始化调用点的一个变量,这个临时变量就是函数调用的结果。
如果函数返回引用,那么这个引用仅仅是它所引用对象的一个别名。

不要返回局部对象的引用或者指针

函数完成后,它所占用的存储空间也被释放掉了(函数的栈帧被释放了)。因此,函数终止意味着局部变量的引用将指向不再有效的内存空间,同样的,返回局部对象的指针也是错误的,函数一旦完成,局部对象被释放,指针将指向一个不存在的对象。

返回类类型的的函数和调用运算符

函数的返回值可以是应用,指针,类等对象,可以使用函数调用的结果访问结果对象的成员。

引用返回左值

函数的返回值类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。

列表初始化返回值

函数可以返回花括号包围的值的列表。这个列表也用来对表示函数返回的临时量进行初始化。如果列表为空,临时量进行值初始化。否则,返回的值由函数的返回类型决定。
如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且这个值所占空间不应该大于目标类型的空间。如果函数返回的是类类型,由类本身定义初始值如何使用。

main函数的返回值

对于main来说,允许它没有return语句直接结束,如果控制流到了main函数的结尾处而没有return语句,编译器将隐式的插入一条返回0的return语句。

递归

如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数。

返回数组指针

因为数组不能被拷贝,所以函数不能返回数组。不过可以返回数组的指针或者引用。

声明一个返回数组指针的函数

返回数组指针的函数形式如下所示:

1
Type (*function(parameter_list)) [dimension]

类似于其他数组的声明,Type表示元素的类型,dimension表示数组的大小,(*function(parameter_list))两端的括号必须在,否则函数的返回类型就是指针的数组。如下示例:

1
int (*func(int i))[10];

func带有参数,说明它是一个函数,前面带有解引用操作,说明可以对函数调用的结果执行解引用操作,括号右面说明这是一个维度为10的数组,括号左面是数组类型。

使用尾置返回类型

可以使用尾置返回类型,任何函数的定义都能使用尾置返回,但是这种形式一般用于比较复杂的返回类型,比如数组的指针或者数组的引用。形式如下:

1
auto func() -> int (*)[10];

使用decltype

可以使用decltype声明返回值类型。比如返回一个指针时,

1
2
3
4
5
6
7
8
int odd[] = {1, 3, 5, 7, 9};
int even[] = {2, 4, 6, 8, 10};


decltype(odd) *func(int i)
{
return (i%2)? &odd: &even;
}

返回指向函数的指针

和数组类似,虽然不能返回一个函数,但是可以返回指向函数类型的指针。**然后,我们必须把返回类型手动写成指针,编译器不会自动的将函数返回类型当成对应的指针类型处理。**最好的办法是使用类型别名:

1
2
using F = int(int*, int);   //F是函数,不是指针
using PF = int(*)(int*, int); //PF是指针类型

必须注意的是,和函数类型的形参不一样,返回类型不会自动的转换成指针,我们必须显式的将返回类型指定为指针。

auto和decltype作用于函数指针

将decltype作用于某个函数时,它返回函数类型而非指针类型,需要我们显式的加上*表示我们需要返回指针,而非函数本身。当decltyp作用于函数指针时,它返回的是函数指针类型。

参考文献

1.《C++ Primer》第五版

UNIX Signals

发表于 2019-12-05 | 更新于 2020-02-24 | 分类于 UNIX

Program Error Signals

当操作系统或者计算机本身检测到一个严重的程序错误时,它就会产生program error signals。一般来说,这些signals都表示你的程序出现了很严重的问题,没有办法继续进行计算了。
一些程序为了在进程终止前进行清理,会建立handle program。这个handler结束的时候应该指定该signal的默认动作或者重新raise这个signal,这样子和原来没有建立signal处理程序的时候相比,仅仅多了清理的一部分,其他还和原来一样。一般来说,这些signal的默认动作都是终止进程。如果不是raise或者kill发出的这些signals,选择block或者忽略这些singals或者建立handlers让它们正常返回,程序可能会崩溃。
当某一个program error signals终止一个进程的时候,它会写一个core dump文件记录进程终止状态。这个core dump文件的名字叫做core,保存在进程终止时所在目录。(在ubuntu中,没有保存文件,而是直接输出了出来[3])

SIGFPE

SIGILL

SIGBUS

SIGSEGV

SIGABRT

SIGTRAP

SIGEMT

Terminal Signals

这一小节介绍的signal都是由于terminate一个进程的。它们之间的区别在于使用目的不同,并且程序可能对不同的signals有不同的处理方法。
处理这些signals的目的是让进程在terminate之前能够进行合适的清理。比如,删除临时文件等等。

SIGINT(程度最轻)

SIGINT是程序中断singal,当用户输入INTR字符(通常是C-c)时中断,发送到前台进程组的所有进程。

SIGTEM(正常的kill)

这个signal可以被blocked, handled和忽略,shell命令的kill(1)默认会产生SIGTERM signal。

It is the normal way to politely ask a program to terminate.

SIGQUIT(可以忽略的最harvest信号)

SIGQUIT和SIGINT很像,会中断一个进程,但是它被QUIT(通常是C-\)控制,发送给前台进程组的所有进程。并且当它terminate一个进程时,它会产生一个core dump,就像一个程序出错信号一样。
在处理SIGQUIT的时候,最好不进行某些cleanups。比如,如果程序创建临时文件,处理其他termination时,最好把临时文件给删除了,但是对于SIGQUIT来说,最好不把它们给删了,因为用户可以用它们查找原因。

SIGKILL(不能被捕捉或者忽略)

SIGKILL signal用于立刻终止程序。这是两个不能被捕捉或者忽略的信号之一(另一个是作业控制信号SIGSTOP)。它向系统管理员提供了一种可以杀死任一进程的可靠方法。
这个singal通常是显式请求。因为他不能被handled,所以一般把它作为最后一个选择,在尝试了C-c或者SIGTERM不起作用之后,最后再使用SIGKILL。

SIGHUP

如果终端接口检测到一个连接断开(可能因为网络或者电话连接断了),就将这个signal报告和这个终端相关的控制进程(会话首进程),会话首进程可能在后台,而上述几个SIGNAL都是发送给前台进程(除了SIGKILL)。

其他

The default behavior of SIGINT, SIGTERM, SIGQUIT and SIGHUP is to kill the program. However applications are allowed to install a handler for these signals. So the actual behavior of applications when they receive these signals is a matter of convention (which each application may or may not follow), not of system design.
SIGINT is the “weakest” of the lot. Its conventional meaning is “stop what you’re doing right now and wait for further user input”. It’s the signal generated by Ctrl+C in a terminal. Non-interactive programs generally treat it like SIGTERM.
SIGTERM is the “normal” kill signal. It tells the application to exit cleanly. The application might take time to save its state, to free resources such as temporary files that would otherwise stay behind, etc. An application that doesn’t want to be interrupted during a critical application might ignore SIGTERM for a while.
SIGQUIT is the harshest of the ignorable signals. It’s meant to be used when an application is misbehaving and needs to be killed now, and by default it traditionally left a core dump file (modern systems where most users wouldn’t know what a core file is tend to not produce them by default). Applications can set a handler but should do very little (in particular not save any state) because the intent of SIGQUIT is to be used when something is seriously wrong.
SIGKILL never fails to kill a running process, that’s the point. Other signals exist to give the application a chance to react.
SIGHUP is about the same as SIGTERM in terms of harshness, but it has a specific role because it’s automatically sent to applications running in a terminal when the user disconnects from that terminal (etymologically, because the user was connecting via a telephone line and the modem hung up). SIGHUP is often involuntary, unlike SIGTERM which has to be sent explicitly, so applications should try to save their state on a SIGHUP. SIGHUP also has a completely different conventional meaning for non-user-facing applications (daemons), which is to reload their configuration file.

Job Control Signals

SIGCLD

SIGCONT

SIGSTOP

SIGSTP

交互停止信号,当用户在终端上按下(C-z)时,终端驱动程序产生这个信号,发送到前台进程组的所有进程。

SIGTTIN

SIGTTOU

参考文献

1.https://unix.stackexchange.com/questions/251195/difference-between-less-violent-kill-signal-hup-1-int-2-and-term-15
2.https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html#Termination-Signals
3.https://stackoverflow.com/questions/2065912/core-dumped-but-core-file-is-not-in-the-current-directory

UNIX signal

发表于 2019-12-04 | 更新于 2020-02-25 | 分类于 UNIX

概述

Signals是软件终端。它提供了一种处理异步事件的方法。

什么是signal

每一个signal都有一个名字,它们以三个字符SIG开头。例如:

  • SIGABRT是abort signal,调用abort函数时产生这种signal
  • SIGALRM是闹钟signal,由alarm函数设置的定时器超时后产生这种signal

在头文件<singal.h>中,signal name都被定义成正整数常量。如果内核包含对用户级应用程序有意义的文件,被认为是一种不好的形式,如果应用程序和内核需要使用同一个定义,那么就将有关信息放在内核头文件中,然后用户级头文件再包含该内核头文件。比如Linux 3.2.0将signal定义在<bits/signum.h>中。

很多条件可以产生signal:

  • 用户按一些键
  • 硬件异常
  • 调用kill(2)函数
  • 调用kill(1)命令,它是kill(2)的接口
  • 检测到某种软件条件已经发生

常见的signal可以分为以下几类:

  • 程序出错signals,用于report程序错误
  • Termination singals,用于中断或者终止程序
  • Alarm signals,
  • 异步I/O signals
  • Joc control signals
  • 操作错误signal
  • miscellaneous signals
  • signal messages

关于具体的每个signal的介绍,可以看书,看文档man 7 signal,或者查看另一篇文章UNIX signals。

在某个signal出现后,可以按照以下三种方式之一进行signal处理:

  1. 忽略singal。有两个signal不能被忽略:SIGKILL和SIGTSTP,它们向内核或者root用户踢欧冠呢了停止或者终止进程的可靠方法。还有某些硬件signal,除零或者非法内存引用,进程的行为是未定义的。
  2. 捕获signal。通过内核接收到某个signal后,调用相应的用户函数。
  3. 执行系统默认动作。对于大多数系统,默认动作是终止进程的执行。

函数signal

signal是ISO C定义的。但是ISO C不涉及多进程,进程组,终端I/O等概念。所以它对signal的定义非常含糊,对于UNIX的用处很小。signal的实现在不同UNIX版本上是不同的,最好使用sigaction函数代替signal。

signal定义

1
2
3
4
#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

signal性质

  1. signal函数有两个参数和一个返回值。第一个参数signum是整形,表示一个signal,第二个参数handler是函数指针,返回值也是一个函数指针。
  2. handler的值是常量SIG_IGN,SIG_DFL或者一个函数的地址。分别表示忽略该信号,执行默认动作,或者调用相应的函数。
  3. **signal函数的返回值是指向之前的信号处理程序的函数指针。**所以,对于signal来说,只有改变信号的处理方式才能确定信号的当前处理方式。

exec和fork

当使用exec执行一个程序时,所有signal的状态都是系统默认或者忽略。exec函数将原先设置为要捕获的signal更改为默认动作,其他signal的状态不变。比如一个进程原先要捕获的signal,执行一个新程序后就不再catch了,因为signal catch函数的地址可能在执行的新程序文件中无意义了。

而fork因为复制了父进程的内存映像,所以信号捕捉函数的地址在子进程中是有意义的,子进程继承了父进程的信号处理方式。

不可靠的signal

之前一些版本的signal是不可靠的,不可靠说的是:

  1. signal会丢失。比如一个signal发生了,但是进程却不知道。
  2. 进程对signal的控制能力很差。进程只能catch或者ignore signal,并不能阻塞signal。
  3. 进程每次接到signal对其进行处理时,然后将signal重置为默认值。
  4. 进程不希望发生某种signal时,不能关闭它,只能ignore它。

中断的系统调用

如果进程在执行一个低速系统调用而阻塞期间捕捉到一个signal,这个系统调用就会被中断不再继续执行(不再阻塞),返回出错,errno设置为EINTR。
什么是低速系统调用?系统调用被分为两类,一类是低速系统调用,另一类是其他系统调用。低速系统调用是指可能会使进程永远阻塞的一类系统调用,比如:

  1. 如果某些类型文件(管道,终端设备和网络设备)的数据不存在,则读操作可能会使调用者永远阻塞。
  2. pause和wait函数等。
  3. …

注意,与磁盘I/O有关的系统调用大多数时候总是会很快返回。

可重入函数

signal发生的时间是任意的,进程正在执行的正常指令可能会被信号处理程序中断,会对进程造成破坏。
SUS说明了在信号处理程序中保证调用安全的函数,这些函数是可重入的,被称为异步信号安全的(async-signal safe)。如下所示是异步信号安全的函数:
re
其余的大多数函数是不可重入的,因为它们可能满足以下条件:

  1. 使用静态数据结构;
  2. 调用malloc或者free;
  3. 是标准I/O函数。

因为每个线程只有一个errno变量,所以信号处理程序可能会修改它的原值。因此,在调用可重入函数之前,应该先保存errno,在调用后恢复errno。

SIGCLD语义

可靠signal

函数

ISO C并不涉及多进程,所以它不能定义以进程ID为参数的函数。

kill和raise

kill向参数pid指定的进程或者进程组发送一个signal。
raise向调用者发送一个signal,ISO C中没有线程,POSIX.1扩展了raise可以处理多线程。单线程程序中等价于kill(getpid(), sig)。在多线程程序中等价于pthread_kill(pthread_self(), sig)。

alarm和pause

alarm不阻塞,当某个时刻到达时,内核会产生一个SIGALRM信号。
pause使调用进程挂起,直到捕捉到任意一个信号,执行相应的信号处理程序,并从其返回时,pause才返回。

参考文献

1.《APUE》第三版
2.https://www.gnu.org/software/libc/manual/html_node/Standard-Signals.html#Standard-Signals

C/C++ function passing arguments

发表于 2019-11-28 | 更新于 2019-12-15 | 分类于 C/C++

参数传递

形参初始化的机理和变量初始化一样。形参的类型决定了形参和实参交互的方式。如形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。
当形参是引用类型时,我们说它对应的实参被引用传递,或者函数被传引用调用。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。
当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递或者函数被传值调用。

因为C中没有引用,所以C中传递参数的方式只有值传递,而C++中还有引用,不仅有值传递,还有引用传递。

值传参和引用传参

值传参

当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值。值传参的原理和这个一样,函数对形参做的所有操作都不会影响实参。

指针形参

指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。但是因为指针可以让我们间接访问它所指向的对象,所以可以通过指针修改它所指对象的值。

引用传参

函数接受的参数是引用类型的话就是引用传参。通过使用引用形参,函数可以改变一个或者多个实参的值。
引用传参的好处:

  1. 避免拷贝,可以避免低效的拷贝操作,或者有些类型不支持拷贝,比如IO类型。
  2. 间接的实现多个返回值(也可以通过值传递指针实现)。

值传参和引用传参的区别

值传参是将原始变量的值拷贝一份给形参,函数对形参的操作不会影响实参(指针可以简介的修改)。
而引用传参是相当于直接把实参的引用给传递了形参,任何对形参的修改都是直接对实参的修改。

const形参和实参

当形参是const时,必须注意顶层const,顶层const作业于对象本身。当用实参初始化形参时,会忽略掉顶层const,即形参的顶层const被忽略掉了。当形参有顶层const时,传递给它常量或者非常量对象都是可以的。

指针或者引用形参和const

形参的初始化方式和变量的初始化方式是一样的,所以指针或者引用形参和const结合时,按照const变量的初始化规则执行就行。

尽量使用常量引用

把函数不会修改的形参定义成普通的引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。(比如,不能把const对象,字面值或者需要类型转换的对象传递给普通的引用传参)

数组形参

数组有两个特殊的性质:

  1. 不允许拷贝,因为不能拷贝数组,所以不能以值传递的方式使用数组参数。
  2. 在使用数组时通常会将其转换成指针。因为数组会被转换成指针,所以为函数传递数组时,实际上传递的是指向数组首元素的指针,这样子可以节约开销。

管理数组转换的指针

当传递给函数一个数组时,实参自动的转成指向数组首元素的指针,数组的大小对于函数的调用没有什么影响。因为数组是以指针的形式传递给函数的,所以函数其实是不知道数组的大小的,调用者应该为此提供一些额外的信息。通常有三种方式:

  1. 显示传递一个表示数组大小的形参
  2. 使用标记指定数组长度,要求数组本身包含一个结束标记,典型的例子是C风格字符串。
  3. 使用标准库规范,传递数组首元素和尾后元素的指针。可以使用begin和end函数获得数组的首元素和尾后元素的指针。

数组形参和const

当函数不需要对数组进行写操作时,数组形参应该是指向const的指针(底层const)。只有当函数确实需要改变数组元素值的时候,才把形参定义成指向非常量的指针。

数组引用形参

C++允许将变量定义成数组的引用,形参也可以是数组的引用。此时,引用形参绑定到数组上。

传递多维数组

C++其实没有真正的多维数组,多维数组其实就是数组的数组。把多维数组传递给函数时,传递的是指向数组首元素的指针。而多维数组是数组的数组,首元素本身就是一个数组,多维数组转换成指向数组的指针。数组第二维以及后面维度都是数组类型的一部分,不能省略。

函数指针形参

和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。这个时候,形参看起来是函数类型,实际上是当成指针使用。
可以直接把函数作为实参使用,这个时候它会自动的转换成指针:

1
2
3
4
5
6
//第三个参数是函数类型,它会自动的转换成指向函数的指针
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
//第三个参数是显式声明的指向函数的指针
void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &));
// 函数lengthCompare会被自动的转换成函数指针
useBigger(s1, s2, lengthCompare);

使用typedef和delctype简化函数指针

可以使用下列语句定义函数指针:

1
2
3
4
5
6
7
8
//下面两个是函数类型:
typedef bool Func(const string&, const string&);
typedef delctype(lengthCompare) Func2;


//下面两个是函数指针类型:
typedef bool (*FuncP)(const string&, const string&);
typedef delctype(lengthCompare) *FuncP2;

需要注意的是,decltype返回函数类型,不会将函数自动转换成指针类型,只有在前面加上*才能得到指针。

可变形参

当不知道向函数传递多少个参数时,C++ 11提供了两种方法处理不同数量实参,如果所有实参类型相同,传递initializer_list标准库类型,如果实参类型不同,编写特殊的函数,可变参数模板。
此外,C++ 还有一种特殊的形参,叫做省略符,可以用它传递可变数量的实参。

initializer_list形参

如果实参数量未知,但是类型相同,可以使用标准库类型initializer_list类型的形参。该标准库提供的操作如下:

  • initializer_list<T> lst,默认初始化,T类型元素的空列表
  • initializer_list<T> lst{a, b, c...},list元素是对应初始值的副本,列表中的元素是const
  • lst2(lst)和lst2 = lst,注意,拷贝或者赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素。
  • lst.size()
  • lst.begin()
  • lst.end()

initializer_list是一个标准库类型,它也是一个模板,它的元素永远都是常量。

有一个疑问,就是std::initializer_list和std::vector有什么区别?[2,3,4]
拷贝std::initializer_list的时候并不会拷贝底层的对象。相当于拷贝了“指针”,或者说std::initializer_list有reference semantics而std::vector具有value semantics。

Initializer lists may be implemented as a pair of pointers or pointer and length. Copying a std::initializer_list does not copy the underlying objects.

省略符形参

省略符形参是为了便于C++访问某些特殊的C代码而设置的。省略符形参只能出现在形参列表的最后一个位置:

1
2
void foo(param_list, ...)
void foo(...);

main命令行选项

详细可以查看C/C++ main argc argv。

参考文献

1.《C++ Primer》第五版
2.https://stackoverflow.com/questions/27753420/initializer-list-vs-vector
3.https://en.cppreference.com/w/cpp/utility/initializer_list
4.https://stackoverflow.com/questions/14414832/why-use-initializer-list-instead-of-vector-in-parameters

C/C++ separate compilation

发表于 2019-11-28 | 更新于 2019-12-10 | 分类于 C/C++

功能文件头文件和实现

头文件

1
2
3
4
5
6
7
// print_string.h
#ifndef PRINT_STRING
#define PRINT_STRING

void print_string(const char *str);

#endif

实现

1
2
3
4
5
6
7
8
// print_string.cpp
#include "print_string.h"
#include <stdio.h>

void print_string(const char *str)
{
printf("%s\n", str);
}

测试文件实现

1
2
3
4
5
6
7
8
9
10
#include "print_string.h"


int main()
{

char str[] = "hello world";
print_string(str);
return 0;
}

编译和链接

执行以下命令

1
2
g++ main.cpp print_string.cpp -o main
./main

理解

在main中包含了print_string.h头文件,相当于对函数进行了声明。然后使用g++编译的时候相当于提供了print_string的实现。

关于分离式编译的原理可以查看CSAPP linking。

Process Relationships

发表于 2019-11-28 | 更新于 2020-01-04 | 分类于 UNIX

概述

在UNIX Process Control中,介绍了:

  1. 每一个进程都有一个父进程,初始的kernel-level的进程通常是它自己。
  2. 当子进程终止的时候,父进程可以获得子进程的exit status。
  3. 同时在介绍waitpid的时候`提到了process groups,并且解释了我们可以等待一个进程组中任意进程的终止。

这一篇文章更详细的介绍了process groups,以及POSIX.1中引入的session的概念。同时还介绍了用于登录的login shell和所有从login shell中启动的进程的关系。

终端登录

BSD登录

系统bootstrap时,内核创建进程号为1的init进程。init进程使系统进行入多用户模式,init读取文件/etc/tty,对每一个允许登录的终端设备,init调用一次fork,它所生成的子程序exec getty程序。
getty对终端设备调用open函数,以读写方式打开终端。一旦终端被打开,文件描述符0,1,2就被关联到该设备。然后getty输出login等字样,等待用户输入。当用户输入username之后,getty工作就结束了,接下来通过类似于以下的方式调用login:

1
execle("/bin/login", "login", "-p", username, (char*)0, envp);

login得到了用户名,接下来调用getpasswd提示用户键入密码,然后调用crypt将用户键入的口令和shadow中的pwsswd比较,判断密码是否正确。如果密码正确的话,login还会进行以下工作:

  1. 将当前工作目录改为用户的主目录。
  2. 调用chown更改终端的控制权,使登录用户成为它的所有者。
  3. 对终端设备的权限改成用户读和写。
  4. 用login得到的所有参数进行初始化
  5. login进程更改登录用户的uid并调用该用户的登录shell。

当然现代的login不仅仅进行这些工作,还会根据启动文件更改或者增加用户的环境变量等等。

网络登录

BSD登录

网络登录的话,BSD中有一个inetd进程,等待绝大多数互联想链接。作为系统启动的一部分,init调用一个shell,使其执行shell脚本/etc/rc,shell脚本启动一个守护进程inetd。当这个shell脚本终止时,inetd的进程变成init。inetd等待TCP/IP连接,每当有一个连接到达时,就执行一次fork,然后使用exec执行相应的子程序。
比如一个TELNET服务请求。客服进程打开一个到服务主机的TCP连接,客户机运行TELNET服务进程(用telnetd表示)。它们之间使用TELNET应用协议通过TCP交换数据。客服进程的用户登录到服务进程所在的主机。
然后telnetd进程打开一个伪终端设备,并且使用fork将它们分成两个进程。父进程处理通过网络的通信,子进程执行login程序。父进程和子进程之间通过伪终端相连接。在调用exec之前,子进程使其文件描述符0,1,2和伪终端相连。登录成功的话,执行和终端操作类似的设置。

当通过终端或者网络登录时,我们得到一个登录shell,它的标准输入,标准输出和标准错误要么连接到一个终端设备,要么连接到一个伪终端设备。

进程组

除了pid,每一个进程还属于一个process group。进程组是一个或者一组进程的集合。他们都是同一个job的进程,每一个进程组都有一个唯一的进程组id,和pid类似,可以存放在pid_t中。函数getpgrp获得process group的ID,getpgid获得指定进程的进程组ID,它们都是SUS定义的。
每个进程组有一个组长进程,组长进程的进程组ID和它的进程ID一样。进程组组长可以创建一个进程组,创建该组中的进程,然后终止。只要在某个进程组中有一个进程存在,那么该进程组就存在,跟其组长是否终止无关。
可以调用setpgid创建一个新的进程组后者加入一个现有的进程组。

1
int setpgid(pid_t pid, pid_t pgid);

setpgid将pid号为pid的进程的进程组ID设置为pgid。当子进程调用了exec之后,父进程就不能修改子进程的进程组ID了。
通常在job control shell中,在fork之后调用此函数,父进程设置子进程的进程组ID,子进程也设置子进程的进程组ID,这两个调用总有一个是重复的,但是可以确保子进程的组ID被正确设置了。

Session

Session是一个或者多个进程组的集合。比如一个session可以有三个进程组:
进程组1:登录shell,
进程组2:proc1, proc2
进程组3:proc3, proc4, proc5
等等。通常一个进程组的进程是由一个shell pipeline生成的。比如上面的进程组可能是通过以下shell命令实现的:
proc1 | proc2 &
proc3 | proc4 | proc5

可以调用setsid创建一个新的session:

1
2
3
#include <unistd.h>

pid_t setsid(void);

这个函数具有以下性质:
如果调用这个函数的进程不是一个进程组的组长,就创建一个新的session:

  1. 该进程变成新的session的session leader,这个session leader是创建该session的进程。注意SUS只说明了session leader,而没有像pid和process gid之类的session id。也就是说session leader是有唯一PID的单个进程,可以将session leader的ID当做session ID。注意什么是session leader,它是一个进程,而session ID是session leader的PID,或者也把session ID较为session leader的process group ID。
  2. 调用进程是新进程组和新session中的唯一一个进程。
  3. 新的session没有controlling terminal。

控制终端

session和process group的一些其他属性:

  • 一个session通常会有一个controlling terminal,通常是终端设备或者伪终端设备。
  • 建立和控制终端连接的session leader被称为controlling process(控制进程)。
  • 一个session中的几个process group可以被分为一个foreground process group(前台进程组)和多个background process group(后台进程组)。
  • 如果一个session有一个controlling terminal,那么它有一个前台进程组,其它进程组为后台进程组。
  • 无论何时键入终端的中断键,ctrl+C,都会将中断信号发送至前台进程组的所有进程。
  • 无论何时键入终端的退出键,ctrl+\,都会将退出信号发送到前台进程组的所有进程。
  • 如果终端接口检测到网络已经断开,将挂断信号发送到session leader。

登录shell属于后台进程组,它是session leader,也就是controlling process。登录时,会自动建立controlling terminal。有时候不管标准输入,标准输出是否重定向,程序都要和控制终端交互,可以open文件/dev/tty。在内核中,/dev/tty是controlling terminal的同义词,如果没有controlling terminal,对于这个设备的open失败。

tcgetpgrp, tcsetpgrp和tcgetsid

函数原型

1
2
3
4
5
6
7
8
#include <unistd.h>

pid_t tcgetpgrp(int fd);
int tcsetpgrp(int fd, pid_t pgrp);

#include <termios.h>

pid_t tcgetsid(int fd);

属性

  1. tcgetpgrp返回前台进程组ID,它与在fd上打开的终端相关联。
  2. 如果进程有一个controlling terminal,这个进程可以调用tcsetpgrp将前台进程组ID设置为pgrpid,fd必须引用该session的controlling terminal。
  3. 可以通过tcgetsid函数获得session leader的进程组ID。

job control

可以在一个终端上启动多个jobs(groups of process),它控制哪个job可以访问终端,哪个job应该在后台运行,job control需要满足以下三个条件:

  1. 支持job control的shell
  2. 内核的终端驱动程序必须支持job contrl
  3. 内核必须支持某些特定的job-control signals。

在shell中使用job contrl,我们可以创建前台的job,也可以创建后台的job,一个job是进程的集合,通常是进程的pipeline,可以在后台运行多个job。
可以通过键入几个特殊字符和终端驱动程序进行交互作用,控制前台进程组的所有进程:

  • 中断,ctrl+C,产生SIGINT
  • 退出,ctrl+\,产生SIGQUIT
  • 挂起,ctrl+Z,产生SIGSTRP

终端驱动程序还需要处理一些情况:

  1. 当后台job试图读取终端时
    如果有一个前台job和多个后台jobs。一般情况下,只有前台job接收终端输入,当后台job试图从终端读取,并不会报错,终端驱动程序会检测这种情况,并且向后台job发出一个SIGTTIN signal。这个signal会停止后台job,shell向有关用户发出通知说你的后台job停止啦!然后用户可以用shell命令将它转换后前台job,从终端读取。
    如果
  2. 当后台job试图写终端时
    当用户禁止后台job向controlling terminal写后,当后台job试图写向标准输出,终端驱动程序识别出这个写操作来自于后台job,向该job发出SIGTTOU signal,阻塞相应的job。当用户使用fg将后台job转换为前台job时,job继续执行。

shell执行过程

有些shell支持job control,比如bash,有些不支持,比如Bourne shell。

执行以下命令:

1
2
ps -o pid,ppid,pgid,sid,comm | cat      // 后台job
ps -o pid,ppid,pgid,sid,comm | cat & // 前台job

不支持job control的shell

在不支持job control的shell中,管道的最后一个进程是shell的子进程,而执行管道中其他命令的进程是该最后进程的子进程。当最后一个进程终止时,shell得到通知。
所有的job的process group id和shell的都一样。
shell_no_job_control

支持job control的shell

而在支持job control的shell中
每一个job都有一个自己的process group id,和shell的不一样。
shell是两个job的父进程。

孤儿进程组

当一个进程的父进程退出之后,而子进程还没有结束,这个进程就成了孤儿进程。进程组也可以是孤儿进程组。
什么是孤儿进程组:
进程组中每个成员的父进程要么是它组内的一个成员,要么不是这个进程组所在session的成员。

FreeBSD实现

每个session都会有一个seesion结构,它包含:

  • s_count
  • s_leader
  • s_ttyvp
  • s_ttyp
  • s_sid,这一部分不是SUS的组成,只有FreeBSD有。

每个终端或者伪终端会在内核中分配一个tty结构,它包含:

  • t_session
  • t_pgrp
  • t_termios
  • t_winsize

每个进程组都包含一个pgrp结构,它包含:

  • pg_id
  • pg_session
  • pg_memebers

每个进程都有一个proc结构,它包含:

  • p_pid
  • p_ptr
  • p_grp
  • p_pglist

进程通过v_node结构体访问/dev/tty。

它们之间的关系如下图所示:
session_and_process_group

参考文献

1.《APUE》第三版

assembly languages

发表于 2019-11-27 | 分类于 汇编

机器指令

计算机指令可以表示为序列化的bits,一般来说,这是一个程序最低级别的表示-每一条指令都等于CPU的一个单个的,不可分隔的指令。这种表示方法叫做机器语言,因为它是机器可以理解的唯一一种形式。

汇编语言

一种更高level的表示叫做assembly language。Assembly language和机器语言的很类似,通常可以很方便的把程序从汇编语言转换成机器语言。因为机器语言和汇编语言的相似性,每一个不同的机器架构都有它自己的汇编语言。事实上,每一个架构都可能有好几个汇编语言。
汇编语言的优势是汇编语言对于人类来说是更容易阅读和理解的。举例来说,将寄存器20和寄存器17的内容相加,并将结果存放在寄存器16的MIPS机器语言指令是:0x02918020。给出这个指令,很难直接理解它是怎么工作的。相同的MISP汇编指令是:
add $16, $20, $17
可读性更强。

C/C++ strlen vs sizeof

发表于 2019-11-26 | 更新于 2019-12-17 | 分类于 C/C++
  1. strlen不计算字符串数组的null字节,而sizeof会计算null字节所占的字节。
  2. strlen是一个函数,使用时需进行一次系统调用。而sizeof会在编译时计算。

参考文献

1.《APUE》第三版8.3节

UNIX struct

发表于 2019-11-25 | 更新于 2019-12-08 | 分类于 UNIX

struct FILE
{

};

struct DIR
{

};

结构体

POSIX定义了
struct dirent
{

};

struct pwd
{

};

struct spwd
{

};

struct grp
{

};

struct timespec
{

};

struct sm{

};

进程资源限制

1
2
3
4
5
struct rlimit{
rlim_t rlim_cur;
rlim_t rlim_max;

};

参考文献

1.《C++ Primer》第五版
2.https://en.cppreference.com/w/c/chrono/timespec
3.https://www.gnu.org/software/libc/manual/html_node/Elapsed-Time.html

1…678…34
马晓鑫爱马荟荟

马晓鑫爱马荟荟

记录硕士三年自己的积累

337 日志
26 分类
77 标签
RSS
GitHub E-Mail
© 2022 马晓鑫爱马荟荟
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Pisces v6.6.0