三、进程
操作系统的诞生就是为了任务管理,而进程与线程就是管理的目标。从某种意义上,操作系统的内存管理、文件系统、I/O,等等都是为了这个目标。所以这也是本书的重中之中。
3.1 文件、进程与用户
让我们先从文件说起。
文件作为一种资源,其实是要被某个进程访问的。这里有个概念要理清楚,就是用户属性和权限。
每个打开的文件,都被分配一个文件描述符,同时可以由stat通过路径和文件名或者fstat通过文件描述符获得一个文件属性结构体:
struct stat{mode_t st_mode;
ino_t st_inod;
……… },
ls -l命令即时通过这个文件结构体,获得相关的文件信息。
其中st_mode包括9各访问权限位屏蔽字,U,G,O。同时包括设置用户ID和设置组ID两个位。
每个进程,则包括6个与用户关联的ID。分别是实际用户ID,组ID。有效用户ID,组ID,附属组ID,保存的设置用户ID,设置组ID。
对于一个要被进程执行的文件,如果该文件的设置用户ID位置位,则进程执行这个文件的时候,该进程具有文件所有者用户的权限。这点要尤其注意!
一个新文件的用户ID设置为创建该文件的进程的有效用户ID。用户组ID,POSIX.1规定了两种可选的方案:要么与进程有效组ID相同,要么与所在目录的组ID。
各种实现方案各有不同,如MAC选择总是与目录组ID相同,而Linux3.2.0则根据目录的设置组ID位是否被设置分别采用不同的策略。
umask函数,设置屏蔽位。屏蔽为1的,st-mode相应位一定会被关闭。通常在shell中用umask命令可以查看该登录shell的umask。
3.2 进程
3.2.1 进程环境
让我们先来理顺一个场景。在shell中执行一个ELF文件或者脚本的时候,会发生什么?
首先,shell进程会调用fork()系统调用创建一个新的进程,然后调用exec族系统调用函数来具体执行。exec类函数会先根据文件头128个字节判断可执行文件的文件类型,然后调用相应的加载器进行文件加载,并执行。这个过程可以参考程序员的自我修养P174页。
这里说的进程环境其实主要指C程序的存储空间布局。
在一个进程的虚拟地址空间内,分布如下:
命令行参数+环境变量
栈
共享库
堆
BSS
初始化的数据
正文
后面两段由exec从程序文件中读入,BSS由exec初始化为0。
然后要注意ISO C的几个函数:
malloc, calloc, realloc,free,genenv, putenv, setenv
由于goto语句不能跨越函数,所以有了setjmp, longjmp。注意配对使用时,寄存器的环境得不到保存,所以尽量使用全局变量,静态变量和volatile变量,不要使用寄存器变量,auto标量。
3.2.2 进程控制
几个特殊进程:进程0(见Linux内核设计的艺术P65),进程1(init进程,/etc/inittab文件或者/etc/init.d中配置初始化文件),进程2(page daemon支持虚拟存储系统的分页操作)
fork
注意,由于UNIX没有提供一个函数可以获得所有子进程的ID,所以父进程的fork返回子进程id。子进程返回0--因为调用getppid就可以获得父进程id。
父进程与子进程不共享除正文段外的其他存储空间(子进程只拥有副本而已)
父子进程共享v节点与文件偏移量。
fork的两个常用用法:
a) 父进程复制自己,子进程处理具体请求,父进程继续等待客户端服务请求。如网络服务
b) 一个进程执行不同的程序。如shell,fork后exec
exit
exit, _Exit是C标准库的,_exit是系统调用。exit会关闭所有打开描述符,释放相关内存
父进程exit后,进程管理程序会遍历所有进程,将父进程id相关的改为1,即由init进程来收养他的子进程
子进程exit后,终止状态,CPU时间总量等依然保存。如果父进程没有wait处理,则变成僵死进程
wait与waited
wait(&stat), waited(pid, &stat, options)
wait阻塞直到第一个子进程终止,stat存放子进程终止状态
exec
execl,execv,execle,execve,execlp,execvp
l代表list,v代表参数,p代表取filename参数,e代表取envp数组
解释器文件
#! pathname [option-argument]
进程调度
nice(), getpriority(), setpriority()
nice值越大表示其越友好,优先级越低
3.2.3 进程关系
1.登录shell进程关系
init进程使用户进入多用户模式,init读取文件/etc/ttys对每个登录的终端设备,调用一次fork,它所生成的子进程则调用exec getty程序。
gettty对终端设备调用open函数,将终端打开,一旦设备打开,fd0,1,2设置到该设备上。然后getty输出login: 之类的信息。当用户键入用户名后,getty工作完成,调用Login程序如下:
execle(“/bin/login”, “login”,”-p”,username, (char *)0, envp);
login得到用户名,调用getpwnam取得用户的口令文件登录项,调用getpass显示Passwd,读密码,调用crypt将口令加密,并与pw_passwd字段相比较。如果密码错误多次,login exit(1), 父进程取得1退出信号,再次调用fork, 执行getty,对此终端重复上述过程
注意这些过程,init1的实际用户和有效用户ID都是0,即超级用户权限,所以子进程结束后,孙子进程被init1收养。这些进程用户权限都是root
如果login登录成功,则:
设置当前目录为用户其实目录
调用chown更改终端权限
设置组ID
初始化系统环境
设置用户ID,并调用该用户的登录shell
2.网络登录
init调用一个shell,由此启用守护进程Inetd,inetd进程等待网络连接。当这个shell终止,inetd被init收养
inetd等待TCP/IP连接,当一个客户端请求到达,执行fork,exec子进程请求服务相关的程序
3.进程组与会话
进程组,各进程由同一作业结合起来,接收来自同一终端的各种信号
每个进程组有一个组长进程,其组ID等于进程ID
一个进程只能为它自己或它子进程设置组ID
通常由shell管道将几个进程编成一组
如果调用setsid的进程不是一个组的组长,则创建一个新会话
与控制终端连接的会话首进程成为控制进程
会话可以包括一个前台进程组,多个后台进程组
额,困了,睡啦- -@