自学 Linux 11—程序、进程和线程之进程的产生方式

进程

每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。其中都有一下这些信息。

  █ 进程 id。系统中每个进程有唯一的 id,在 C 语言中用 pid_t 类型表示,其实就是一个非负整数
  █ 进程的状态,有运行、挂起、停止、僵尸等状态
  █ 进程切换时需要保存和恢复的一些 CPU 寄存器。
  █ 描述虚拟地址空间的信息。
  █ 描述控制终端的信息。
  █ 当前工作目录(Current Working Directory)
  █ umask 掩码。
  █ 文件描述符表,包含很多指向 file 结构体的指针。
  █ 和信号相关的信息。
  █ 用户 id组 id
  █ 控制终端、Session 和进程组
  █ 进程可以使用的资源上限(Resource Limit)

进程产生的方式

  进程是计算机中运行的基本单位。要产生一个进程,有多种产生方式,例如使用 fork() 函数、system() 函数、exec() 函数等,这些函数的不同在于其运行环境的构造之间存在差别,其本质都是对程序运行的各种条件进行设置,在系统之间建立一个可以运行的程序

一、进程号

  每个进程在初始化的时候,系统都分配了一个 ID 号,用于标识此进程。

  在 Linux进程号是唯一的,系统可以用这个值来表示一个进程,描述进程的 ID 号通常叫做 PID,即进程 IDprocess id)。PID 的变量类型为 pid_t

1. getpid()、getppid() 函数介绍

  getpid() 函数返回当前进程ID 号,getppid() 返回当前进程的父进程ID 号。类型 pid_t 其实是一个 typedef 类型,定义为 unsigned intgetpid() 函数和 getppid() 函数的原型如下:

#include <sys/types.h>
#include <unistd.h> 
pid_t getpid(void); 
pid_t getppid(void);
2. getpid() 函数的例子

  下面是一个使用 getpid() 函数和 getppid() 函数的例子。程序获取当前程序的 PID 和父程序的 PID

#include <sys/types.h>
#include <unistd.h>
#include<stdio.h> 
int main()
{
	pid_t pid,ppid; /* 获得当前进程和其父进程的ID号 */ 
	pid = getpid(); 
	ppid = getppid();
	printf("当前进程的 ID 号为:%d\n",pid); 
	printf("当前迸程的的父进程号 ID 号为:%d\n",ppid);
	return 0;
}

  对上述程序进行编译,在系统上进行运行,其结果为:

当前进程的ID号为:16957 
当前进程的的父进程号ID号为:16878

  可以知道,进程的 ID 号为 16957,其父进程的 ID 号为 16878

二、进程复制 fork()

  产生进程的方式比较多,fork() 是其中的一种方式。fork() 函数以父进程为蓝本复制一个进程,其 ID 号和父进程 ID 号不同。在 Linux 环境下,fork() 是以写复制实现的,只有内存等与父进程不同时,其他与父进程共享,只有在父进程或者子进程进行了修改后,才重新生成一份。

1. fork() 函数介绍

  fork() 函数的原型如下,当成功时,fork() 函数的返回值是进程的 ID;失败则返回 -1

#include <sys/types.h> 
#include <unistd.h> 
pid_t fork(void);

  fork() 的特点是执行一次,返回两次。在父进程和子进程中返回的是不同的值,父进程中返回的是子进程的 ID 号,而子进程中则返回 0

2. fork() 函数的例子

  下面是一个使用 fork() 函数的例子。在调用 fork() 函数之后,判断 fork() 函数的返回值:如果为 -1,打印失败信息;如果为 0,打印子进程信息;如果大于 0,打印父进程信息。

#include <stdio.h> 
#include <stdlib.h> 
#include <unistd.h>
#include <sys/types.h> 
int main(void)
{
	pid_t pid;
	/* 分叉进程 */ 
	pid = fork();
	
	/* 判断是否执行成功 */ 
	if(-1 == pid) 
	{
		printf("进程创建失败!\n");
		return -1;
	} 
	else if(pid == 0)
	{
		/* 子进程中执行此段代码 */
		printf("子进程,fork 返回值: %d, ID: %d, 父进程 ID: %d\n",pid,getpid(),getppid());
	}
	else
	{
		/* 父进程中执行此段代码 */
		printf("父进程,fork 返回值: %d, ID: %d,父进程 ID: %d\n",pid, getpid(),getppid());
	}
	return 0;
}

  执行此段程序的结果为:

父进程,fork 返回值:17025, ID:17024, 父进程 ID:16878 
子进程,fork 返回值:0, ID: 17025, 父进程ID: 17024

  Fork 出来的子进程的父进程 ID 号是执行 fork() 函数的进程的 ID 号。

三、system() 方式

  system() 函数调用 shell 的外部命令在当前进程中开始另一个进程。

1. system() 函数介绍

  system() 函数调用 “/bin/sh -c command” 执行特定的命令,阻塞当前进程直到 command 命令执行完毕。
  system() 函数的原型如下:

#include <stdlib.h>
int system(const char *command);

  执行 system() 函数时,会调用 fork()execve()waitpid() 等函数,其中任意一个调用失败,将导致 system() 函数调用失畋。
  system() 函数的返回值如下:

  █ 失败,返回 -1
  █ 当 sh 不能执行时,返回 127
  █ 成功,返回进程状态值。

2. system() 函数的例子

  例如下面的代码获得当前进程的 ID,并使用 system() 函数进行系统调用 ping 网络上的某个主机,程序中将当前系统分配的 PID 值和进行 system() 函数调用的返回值都进行了打印:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h> 
int main()
{
	int ret;
	
	printf("系统分配的进程号是:%d\n", getpid());
	ret = system("ping www.baidu.com -c 2");
	printf("返回值为:%d\n",ret); 
	return 0;
}

  对上述代码进行进行编译,执行编译后的程序,其执行结果为:

系统分配的进程号是:17068
PING www.a.shifen.com (61.135.169.125) 56(84) bytes of data.
64 bytes from 61.135.169.125: icmp_req=l ttl=128 time=13.2 ms 
64 bytes from 61.135.169.125: icmp_req=2 ttl=128 time=12.8 ms
--www.a.shifen.com ping statistics --
2 packets transmitted, 2 received, 0% packet loss, time 7124ms 
rtt min/avg/max/mdev = 12.840/13.058/13.276/0.218 ms 
返回值为:0

  系统分配给当前进程的 ID 号为 17068;然后系统 ping 了网络上的某个主机,发送和接收两个 ping 的请求包,再退出 ping 程序;此时系统的返回值在原来的程序中才返回,在测试的时候返回的是 0

四、进程执行 exec() 函数系列

  在使用 fork() 函数和 system() 函数的时候,系统中都会建立一个新的进程,执行调用者的操作,而原来的进程还会存在,直到用户显式地退出;而 exec() 族的函数与之前的 fork()system() 函数不同,exec() 族函数会用新进程代替原有的进程,系统会从新的进程运行,新进程的 PID 值会与原来进程的 PID 值相同。

1. exec() 函数介绍

  exec() 族函数共有 6 个,其原型如下:

#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg,...);
int execle(const char *path, const char *arg, ..., char *const argv[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

  上述 6 个函数中,只有 execve() 函数是真正意义上的系统调用,其他 5 个函数都是在此基础上经过包装的库函数。

  exec 族:

l 命令行参数列表
p 搜素 file 时使用 path 变量
v 使用命令行参数数组
e 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量

  上述的 exec() 函数族的作用是:在当前系统的可执行路径中根据指定的文件名来找到合适的可执行文件名,并用它来取代调用进程的内容,即在原来的进程内部运行一个可执行文件。上述的可执行文件既可以是二进制的文件,也可以是可执行的脚本文件

  与 fork() 函数不同,exec() 函数族的函数执行成功后不会返回,这是因为执行的新程序己经占用了当前进程的空间和资源,这些资源包括代码段、数据段和堆栈等,它们都已经被新的内容取代,而进程的 ID 等标识性的信息仍然是原来的东西,即 exec() 函数族在原来进程的壳上运行了自己的程序,只有程序调用失败了,系统才会返回 -1

  使用 exec() 函数比较普遍的一种方法是先使用 fork() 函数分叉进程,然后在新的进程中调用 exec() 函数,这样 exec() 函数会占用与原来一样的系统资源来运行。

  Linux 系统针对上述过程专门进行了优化。由于 fork() 的过程是对原有系统进行复制,然后建立子进程,这些过程都比较耗费时间。如果在 fork() 系统调用之后进行 exec() 系统调用,系统就不会进行系统复制,而是直接使用 exec() 指定的参数来覆盖原有的进程。上述的方法在 Linux 系统上叫做 “写时复制” ,即只有在造成系统的内容发生更改的时候才进行进程的真正更新。

2. execve() 函数的例子

  execve() 函数的例子如下。例子程序中先打印调用进程的进程号,然后调用 execve() 函数,这个函数调用可执行文件 “/bin/ls” 列出当前目录下的文件。

#include<stdio.h>
#include<unistd.h> 
int main(void)
{
	char *args[] = {"/bin/ls",NULL};
	
	printf("系统分配的进程号是:%d\n",getpid()); 
	if(execve("/bin/ls",args,NULL)<0)
	{
		printf("创建进程出错!\n");
	}
	
	return 0;
}

  调用 exec 后,原来打开的文件描述符仍然是打开的。利用这一点可以实现 I/O 重定向。先看一个简单的例子,把标准输入转成大写然后打印到标准输出:

/* upper.c */
#include <stdio.h>

int main(void)
{
	int ch;
	while((ch = getchar()) != EOF) {
		putchar(toupper(ch));
	}
	return 0;
}
/* wrapper.c */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
	int fd;
	if (argc != 2) {
		fputs("usage: wrapper file\n", stderr);
		exit(1);
	}
	fd = open(argv[1], O_RDONLY);
	if(fd<0) {
		perror("open");
		exit(1);
	}
	dup2(fd, STDIN_FILENO);
	close(fd);
	execl("./upper", "upper", NULL);
	perror("exec ./upper");
	exit(1);
}

  wrapper 程序将命令行参数当作文件名打开,将标准输入重定向到这个文件,然后调用 exec 执行 upper 程序,这时原来打开的文件描述符仍然是打开的,upper 程序只负责从标准输入读入字符转成大写,并不关心标准输入对应的是文件还是终端。运行结果如下:

$ cat test.txt 
abcdefghijklmnopqrstuvwxyz
$ gcc upper.c -o upper
$ gcc wrapper.c 
$ ./a.out test.txt 
ABCDEFGHIJKLMNOPQRSTUVWXYZ

五、所有用户态进程的产生进程 init

  在 Linux 系统中,所有的进程都是有父子或者堂兄关系的,除了初始进程 init,没有哪个进程与其他进程完全独立。系统中每个进程都有个父进程,新的进程不是被全新地创建,通常是从一个原有的进程进行复制或者克隆的。

  Linux 操作系统下的每一个进程都有一个父进程或者兄弟进程,并且有自己的子进程。 可以在 Linux 下使用命令 pstree 来査看系统中运行的进程之间的关系,如下所示。可以看出,init 进程是所有进程的祖先,其他的进程都是由 init 进程直接或者间接 fork() 出来的。

init———NetworkManager———dhclient 
	|				  |——dnsmasq
	|				  |——2*[{NetworkManager}]
	|——account s-daemon----{accounts-daemon}
	|——acpid
	|——at-spi-bus-laun————2*[{at-spi-bus-laun}]
	|——atd
	|——avahi-daemon————avahi-daemon
	|——bamfdaemon——————2*[{bamfdaemon}]
	|——bluetoothd 
	|——colord——————2*[{colord}]
	|——console-kit-dae——————64*[{console-kit-dae}]
	|——cron
	|——cupsd
	|——2*[dbus-daemon]
	|—dbus-launch
	|——dconf-service—————2*[{dconf-service}]
	|——gconfd-2
	|——geany——————bash——————pstree
	|	  |——geany
	|	  |——3*[{geany}]
	|——geoclue-master 
	|——6*[getty]
	|——gnome-keyring-d——————6*[{gnome-keyring-d}]
	|——goa-daemon——————{goa-daemon}
	|——gvfs-afc-volume——————{gvfs-afc-volume}
	|——gvfs-fuse-daemo——————4*[{gvfs-fuse-daemo}]
	|——gvfs-gdu-volume
	|—gvfs-gphoto2-vo 
	|—gvfsd
	|——......

六、wait/waitpid

  僵尸进程子进程退出,父进程没有回收子进程资源(PCB),则子进程变成僵尸进程

  孤儿进程父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为 1 号进程 init 进程,称为 init 进程领养孤儿进程

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);、

waitpid 返回值的四种情况:
< -1 	回收指定进程组内的任意子进程
-1 		回收任意子进程
0 		回收和当前调用 waitpid 一个组的所有子进程
> 0 	回收指定 ID 的子进程

options 取值:
WNOHANG 没有子进程退出立马返回

  一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的 PCB 还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用 waitwaitpid 获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在 Shell 中用特殊变量 $? 查看,因为 Shell 是它的父进程,当它终止时 Shell 调用 waitwaitpid 得到它的退出状态同时彻底清除掉这个进程。

  如果一个进程已经终止,但是它的父进程尚未调用 waitwaitpid 对它进行清理,这时的进程状态称为僵尸(Zombie)进程任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了

  为了观察到僵尸进程,自己写一个不正常的程序,父进程 fork 出子进程,子进程终止,而父进程既不终止也不调用 wait 清理子进程:

#include <unistd.h>
#include <stdlib.h>

int main(void)
{
	pid_t pid=fork();
	if(pid<0) {
		perror("fork");
		exit(1);
	}
	if(pid>0) {
		/* parent */
		while(1);
	}
	/* child */
	return 0;
}

  若调用成功则返回清理掉的子进程 id,若调用出错则返回 -1。父进程调用 waitwaitpid 时可能会:

  █ 阻塞:如果它的所有子进程都还在运行。
  █ 带子进程的终止信息立即返回(如果一个子进程已终止,正等待父进程读取其终止信息)。
  █ 出错立即返回(如果它没有任何子进程)。

  waitwaitpid 这两个函数的区别是:

  █ 如果父进程的所有子进程都还在运行,调用 wait 将使父进程阻塞,而调用 waitpid 时如果在 options 参数中指定 WNOHANG 可以使父进程不阻塞而立即返回 0
  █ wait 等待第一个终止的子进程,而 waitpid 可以通过 pid 参数指定等待哪一个子进程。

  可见,调用 waitwaitpid 不仅可以获得子进程的终止信息,还可以使父进程阻塞等待子进程终止,起到进程间同步的作用。如果参数 status 不是空指针,则子进程的终止信息通过这个参数传出,如果只是为了同步而不关心子进程的终止信息,可以将 status 参数指定为 NULL

  示例:

/* waitpid */
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	pid_t pid;
	pid = fork();
	if (pid < 0) {
		perror("fork failed");
		exit(1);
	}
	if (pid == 0) {
		int i;
		for (i = 3; i > 0; i--) {
			printf("This is the child\n");
			sleep(1);
		}
		exit(3);
	}
	else {
		int stat_val;
		waitpid(pid, &stat_val, 0);
		if (WIFEXITED(stat_val)) //WIFEXITED 这个宏函数用来判断是否是正常退出
			//WEXITSTATUS 这个宏函数用来获取 stat_val 的值
			printf("Child exited with code %d\n", WEXITSTATUS(stat_val));
		else if (WIFSIGNALED(stat_val))
			printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val));
	}
	return 0;
}

  wait 阻塞函数,阻塞等待子进程结束。waitpid 4 种情况 < -1 、= -1、 = 0 、> 0。

进程的退出状态
非阻塞标志,WNOHANG
获取进程退出状态的函数见 manpages
调用进程若无子进程,则 wait 出错返回
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值