1. 进程标识
每个进程都用一个唯一的非负整数标识,即为进程id:pid。进程ID是可以复用的,当一个进程终止时,其进程ID就可以用来标识其他进程。
系统中有一些专用进程:
-
ID为0的进程称为调度进程(也称为交换进程swapper)。该进程为内核的一部分,他并不执行任何磁盘上的程序,因此也被称为系统进程
idle 进程或因为历史的原因叫做swapper进程。它是在 linux 的初始化阶段从无到有的创建的一个内核线程。这个祖先进程使用静态分配的数据结构。
在多处理器系统中,每个CPU都有一个进程0,主要打开机器电源,计算机的BIOS就启动一个CPU,同时禁用其他CPU。运行的CPU 上的swapper进程初始化内核数据结构,然后激活其他的并且使用copy_process()函数创建另外的swapper进程,把0 传递给新创建的swapper进程作为他们进程的PID. -
ID为1的进程称为init进程,在自举过程结束后由内核调用。init进程绝对不会终止,他是一个普通的用户进程(区别于swapper进程,它不是内核中的系统进程),但是它以超级用户权限运行。
由进程0创建的内核线程执行init() 函数,init() 一次完成内核的初始化。init()调用execve()系统调用装入可执行程序init ,结果 ,init 内核线程变成一个普通的进程,且拥有自己的每个进程内核数据结构。在系统关闭之前,init 进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。
init进程由0进程创建,完成系统的初始化,是系统中所有其他用户进程的祖先进程
Linux中的所有进程都是由init进程创建并运行的。首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统启动完成后,init将变成为守护进程监视系统其他进程。
所以说init进程是Linux系统操作中不可缺少的程序之一,如果内核找不到init进程就会试着运行/bin/sh,如果运行失败,系统的启动也会失败。
通过以下函数获取一个进程的pid等id信息:
pid_t getpid(void); // 调用进程pid
pid_t getppid(void); // 调用进程的父进程pid
uid_t getuid(void); // 调用进程的实际用户id
uid_t geteuid(void); // 调用进程的有效用户id
gid_t getgid(void); // 调用进程的实际组id
gid_t getegid(void); //调用进程的有效组id
2. fork函数
一个现有进程可以通过调用fork复刻一个新进程
pid_t fork(void);
fork函数调用一次返回两次。对于子进程返回值是0;对于父进程返回值是子进程pid。子进程只能有一个父进程,通过getppid获得父进程pid。
子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本,子进程获得父进程的数据空间、堆和栈的副本。注意这是子进程所拥有的副本,父子进程并不共享这些存储空间部分。父进程和子进程共享正文段.text。
由于fork之后进程跟着exec,所以现在很多fork实现并不执行父进程数据段、堆和栈的完全副本,而是使用写时复制(copy-on-write)。这些区域由父子进程共享,并且内核将它们的访问权限修改为只读。如果父子进程中的一个试图修改这些区域,则内核只为修改区域的那块内存制作副本。
另外,fork创建的子进程只保留调用线程的副本。也就是说除了调用fork的线程外,其他线程在子进程中“蒸发”了。
文件共享:
父进程的所有打开文件描述符都被复制到子进程中,就好像执行了dup函数。父进程和子进程每个相同的打开描述符共享一个文件表项。如下图所示
可以看出,fork之后的父子进程每个相同的打开描述符共享一个文件表项,因此也共享相同的文件偏移量(读写指针)。
除了打开文件之外,父进程的很多其他属性也由子进程继承
- 实际用户ID、实际组ID、有效用户ID、有效组ID
- 附属组ID
- 进程组ID
- 会话ID
- 控制终端
- 设置用户ID标志和设置组ID标志
- 当前工作目录
- 根目录
- 文件模式创建屏蔽字
- 信号屏蔽和安排
- 对任一打开文件描述符的执行时关闭标志(close-on-exec)
- 环境
- 连接的共享存储段
- 存储映像
- 资源限制
父子进程的区别:
- fork返回值不同
- 进程ID不同
- 子进程的tms_utime、tms_stime、tms_cutime、tms_ustime值设置为0
- 子进程不继承父进程设置的文件锁
- 子进程的未处理闹钟被清除
- 子进程的未处理信号集设置为空集
fork失败原因:
- 系统中已经有了太多的进程
- 该实际用户ID的进程总数超过了系统限制
fork常用于以下方法:
- 一个父进程希望复刻自己,使父进程和子进程同时执行不同的代码段。在网络服务器中较为常见:父进程等待客户端的服务请求,请求到达时fork使子进程处理此请求,父进程则继续等待下一个服务请求。
- 一个进程要执行一个不同的程序,shell常使用这种方式。子进程从fork返回后立即调用exec。有些操作系统将fork之后立刻exec组合成一个操作:spawn,但是UNIX将这两个操作分开,因为子进程可以在fork和exec之间更改自己的属性。
3. vfork函数(不推荐使用)
vfork函数调用方式和fork相同,但语义不同。现在不应该使用这个函数
vfork用于创建一个新进程,而该新进程的目的就是exec一个新程序。但是它不会将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit)。不过在子进程exec或exit之前,它在父进程的空间中运行。这种方式提高了效率,但是如果子进程修改了数据(子进程修改数据会影响到父进程,因为共享存储空间)、进行函数调用、或者没有调用exec或exit就返回可能会带来未知结果。
并且vfork保证子进程先运行,在子进程调用exec或exit之后父进程才可能被调度运行。即子进程调用这两个函数中的任意一个时,父进程会恢复运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步操作,会导致死锁)。
int main(int argc, char* argv[]) {
int num = 1;
if(vfork() == 0) {
// 子进程
cout << "子进程执行" << endl;
num ++;
cout << "子进程终止" << endl;
_exit(0);
} else {
// 父进程
cout << "父进程执行" << endl;
cout << "num : " << num << endl;
cout << "父进程终止" << endl;
}
}
$ ./a.out
> 子进程执行
> 子进程终止
> 父进程执行
> num : 2
> 父进程终止
可以看出子进程修改变量值影响了父进程中该变量,这是因为父子进程共享存储空间。
4. shell运行程序流程
https://blog.51cto.com/dengxi/1696978
-
读取用户由键盘输入的命令行。
-
分析命令,以命令名作为文件名,并将其它参数改造为系统调用execve()内部处理所要求的形式。
-
终端进程调用fork()建立一个子进程。
-
终端进程本身用系统调用wait4()来等待子进程完成(如果是后台命令,则不等待)。当子进程运行时调用execve(),子进程根据文件名(即命令名)到目录中查找有关文件(这是命令解释程序构成的文件),将它调入内存,执行这个程序(解释这条命令)。
-
如果命令末尾有&号(后台命令符号),则终端进程不用系统调用wait4( )等待,立即发提示符,让用户输入下一个命令,转⑴。如果命令末尾没有&号,则终端进程要一直等待,当子进程(即运行命令的进程)完成处理后终止,向父进程(终端进程)报告,此时终端进程醒来,在做必要的判别等工作后,终端进程发提示符,让用户输入新的命令,重复上述处理过程。
shell如何启动:
shell是用户和Linux内核之间的接口程序,为用户提供使用操作系统的接口。它接收用户命令,然后调用相应的应用程序
shell在你成功地登录进入系统后启动,并始终作为你与系统内核的交互手段直至你退出系统
5. exit函数
进程有5种正常终止和3种异常终止。其中正常终止:
- main函数内执行return语句,等效于调用exit
- 调用exit函数,此操作调用终止处理程序(atexit登记的函数),关闭所有标准I/O流,然后调用_exit
- 调用_exit或_Exit。其操作提供一种无需运行终止处理程序或信号处理程序而终止的方法。(exit是标准C库中的一个函数,_exit是一个系统调用)
- 进程的最后一个线程在启动例程中执行return语句。但是,该线程返回值不用做进程的返回