前面吹水了一节,这次来一点干货把,争取一篇搞定。

2.1 进程标识

每个进程都有一个非负整数表示的唯一进程ID。因为进程ID标识符总是唯一的,常将其用作其他表示符的一部分以保证其唯一性。

虽然是唯一的,但是进程ID是可复用的,当一个进程终止后,其进程ID就成为复用的候选者。大多数UNIX系统实现延迟复用算法,使得赋予新进程的ID不同于最近终止进程所使用的ID。这防止了将新进程误认为是使用同一ID的某个已终止的先前进程。

系统中有一些专用进程,但具体细节随实现而不同,ID为0进程通常是调度进程,常常被称为交换进程。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程。

进程ID1通常是init进程,在自举过程结束时由内核调用,该进程的程序文件在早期版本是/etc/init。在较新的版本中是/sbin/init。此进程负责在自居内核后启动一个Unix系统。init进程通常读取与系统有关的初始化文件(/etc/rc*或者/etc/inittab文件,以及在/etc/init.d中的文件),并将系统引导到一个状态(如多用户)。init进程不会终止,会成为所有孤儿进程的父进程。

表示相关的函数:

pid_t  getpid(void);		//获取进程的ID
pid_t  getppid(void);		//获取父进程的ID
uid_t	getuid(void);		//进程的实际用户ID
uid_t 	geteuid(void);		//有效用户ID
gid_t	getgid(void);		//实际组ID
git_t	getegid(void);		//有效组ID
2.2 进程函数

2.2.1 fork

大名鼎鼎的fork函数,一个现有的进程可以调用fork函数创建一个新进程:

pid_t  fork(void);

由fork创建的新进程被称为子进程,fork函数被调用一次,但返回两次。

子进程的返回值为0,而父进程的返回值则是新建子进程的ID。(子进程可以通过getppid获得父进程的进程ID)

子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆,和栈的副本。注意,这是子进程所拥有的副本,父进程和子进程并不共享这些存储空间部份。父进程和子进程共享正文段。

由于在fork之后经常跟着exec,所以现在很多实现并不执行一个父进程数据段、栈和堆的副本。作为替代,使用了写时复制技术。这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读。如果父进程和子进程中任一个试图修改这些区域,则内核值修改区域的那本内存制作一个副本,通常是虚拟存储系统的一页。

int main(void)
{
	pid_t pid;

	if((pid = fork()) < 0 ) {
		printf("fork error\n");
	} else if(pid == 0) {
		printf("child\n");
	} else {
		printf("parent %d\n", pid);
	}

}

一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的,这要看内核。

如果在fork函数调用之前打开了一个文件,那这个文件的inode结点是父子进程共享的,这时候需要加同步操作,如果同时操作会使内容错乱。处理方法,是等待子进程处理完,父进程在处理这个文件,或者子进程exec执行其他程序,相互不影响。

2.2.2 vfork

vfork函数用于创建一个新进程,而该新进程的目的是exec一个新程序,但vfork机制并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec,于是也就不会引用该地址空间(是优化了)。不过在子进程调用exec或exit之前,它在父进程的空间运行,所以不能修改父进程的变量,需要特别注意。

vfork保证子进程先运行,在调用exec或exit之后父进程才可能被调度运行,如果子进程依赖父进程才运行,会导致死锁,这个也要特别注意。

2.2.3 exit

前面已经讲过了进程的退出条件,这里就不在描述了,但是一个进程的借宿,我们都要为这个进程中所打开的描述符进行关闭,还有释放它所使用的内存。

如果一个子进程退出了,该进程的父进程都可以用wait或waitpid来接收子进程的终止状态。

但是如果父进程在子进程之前终止,那这些子进程的父进程都改变为init进程,init进程负责接收这些进程的退出状态和销毁这些子进程。

如果一个子进程已经终止,父进程还没对这个子进程做善后处理,这是这个子进程称为僵死进程,ps看到僵死进程的状态是Z.

2.2.4 wait和waitpid

当一个进程正常或者异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止时个异步事件,所以这种信号也是内核向父进程发的异步通知,

父进程可以忽略该信号,或者提供一个清理函数绑定到这个信号的回到函数中。

调用wait和waitpid可以会发生什么

  • 如果其所有子进程都在运行,则阻塞
  • 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回
  • 如果它没有任何的子进程,则立即出错返回

如果进程由于接收到SIGCHLD信号而调用wait,我们期望wait会立即返回,如果在随机时间点调用wait,进程可能会阻塞。

pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);

对于waitpid函数中pid的参数作用:

pid=-1 等待任一自己承诺函

pid>0 等待进程ID与pid相等的子进程

pid==0 等待组ID等于调用进程组ID的任一子进程

pid<-1 等待组ID等于pid绝对值的任一子进程

2.2.5 waitid

还有另一个取到进程终止状态的函数,

int  waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

2.2.6 wait3和wait4

pid_t wait3(int *statloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);

最后一个参数允许内核返回由终止进程及其所有子进程使用的资源情况。

2.2.7 exec

但进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行,因为调用exec并不创建进程,所以前后的进程ID并未改变,exec值是用磁盘上的一个程序替换了当前进程的正文段。数据段、堆和栈段。

int  execl(const char *pathname, const char *arg0, ...);
int  execv(const char *pathname, char *const argv[]);
int  execle(const char *pathname, const char *arg0, ...);
int  execve(const char *pathname, char *const argv[], char *const envp[]);  //系统调用
int  execlp(const char *filename, const char *arg0, ...);
int  execvp(const char *filename, char *const argv[]);
int  fexecve(int fd, char *const argv[], char *const encp[]);

2.2.8 更改用户ID和更该组ID

int  setuid(uid_t uid);
int  setgid(git_t gid);

2.2.9 system

int system(const char *cmdstring);

system函数执行cmdstring命令的,比较方便,接下来我们先看看system函数的实现

int  system(const char *cmdstring)
{
	pid_t pid;
	int status;
	
	if(cmdstring == NULL) {
		return 1;
	}
	
	if((pid = fork()) < 0)  {
		return -1;
	} else if(pid == 0)  {
		execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
		_exit(127);		//子进程退出也不会冲刷
	} else {
		while(waitpid(pid, &status, 9) < 0) {
			if(errno != EINTR) {
				status = -1;
				break;
			}
		}
	}
	return status;
}

看到上面的实现,我们发现system在实现中调用了fork、exec和waitpid,因此有3种返回值。

(1)fork失败或者waitpid返回除EINTR之外的出错,则system返回-1,并且设置error以指示错误类型。

(2)如果exec失败(表示不能执行shell),则其返回值如同shell执行了exit(127)一样。

(3)否则所有3个函数(fork.exec和waitpid)都成功,那么system的返回值是shell的终止状态。

2.2.10 用户标识

char  *getlogin(void);

2.2.11 进程调度

进程可以通过调整nice值选择以更低优先级运行(通过调整nice值降低它对CPU的占有,因此该进程是“友好的”)

nice值越小,优先级越高,NZERO是系统默认的nice值。

int nice(int  incr);
int  getpriority(int which, id_t who);		//获取进程的nice值,还可以获取一组相关进程的nice值
int  setpriority(int which, id_t who, int value);	//

2.2.12 进程时间

clock_t  times(struct tms *buf);
strcut tms  {
  clock_t tms_utime;		//user CPU time
  clock_t tms_stime;		//system CPU time
  clock_t tme_cutime;		//user CPUtime
  clock_t tms_cstime;		//system CPU time
};
2.3 登录

2.3.1 终端登录

进程/线程/协程(二、进程)_进程特性

当系统自举时,内核创建进程ID为1的进程,也就是init进程,init进程使系统进入多用户模式。

init读取文件/etc/ttys,对每一个允许登录的终端设备,init调用一次fork,它所生成的子进程则执行exec getty程序。

getty对终端设备调用open函数,以读写方式将终端打开。一旦设备打开,则文件描述符0、1、2就被设置到该设备,然后getty输出“login:”登录信息,当用户键入了用户名后,getty的工作就完成了,然后以execle的方式调用login程序。

2.3.2 网络登录

在上节所述中,init知道哪些终端设备可用来登录,并为每一个设备生成一个getty进程(串口),但是对网路登录情况则有所不同,所有登录都经有内核的网络接口驱动陈旭,而且是先不知道有多少这样的登录。因此必须等待一个网络连接请求的到达,而不是是一个进程等待每一个可能的登录。
进程/线程/协程(二、进程)_进程特性_02

init调用一个shell,执行shell脚本/etc/rc。由此shell脚本启动一个守护进程inted。一旦shell脚本终止,inted的父进程就变成了init。inetd等待TCP连接,当一个请求到达,就会执行一次fork,然后在子进程中exec运行telnet程序,就行交互。

2.4 进程关系

2.4.1 进程组

每个进程除了有一个进程ID之外,还属于一个进程组。进程组是一个或多个进程的集合。通常,他们是在同一作业中结合起来的。同一个进程组中的各进程接收来自同一终端的各种信号。每个进程组有一个唯一的进程组ID,也是一个正整数。

pid_t getpgrp(void);	//返回调用进程的进程组ID

每一个进程组都有一个组长进程,组长进程的进程组ID等于其进程ID。

int setpgid(pid_t pid, pid_t pgid);		//可以加以一个现有的进程组或者创建一个进程租

2.4.2 会话

会话是一个或多个进程组的集合。

pid_t  setsid(void);		//建立一个新会话
pid_t  getsid(pid_t pid);	//返回调用进程的会话首进程ID

2.4.3 控制终端

一个会话可以有一个控制终端

建立与控制终端连接的会话首进程被称为控制进程

一个会话中几个进程组可被分成一个前台进程,以及一个或多个后台进程。

如果一个会话有一个控制终端,则它有一个前台进程组,其他进程组为后台进程组

无论何时键入终端的中断键,都会将中断信号发送至前台进程组的所有进程

无论何时键入终端退出键,都将退出信号发送至前台进程组的所有进程。