文章目录结构

(1) 进程控制(创建/中止/切换)

1.进程控制

进程控制的主要任务就是系统使用一些具有特定功能的程序端来创建、撤销进程以及完成进程各状态之间的转换,从而达到多进程、高效率、并发的执行和协调,实现资源共享的目的。

1.1) 进程标识符

进程标识符(PID)是一个进程的基本属性,其作用类似于每个人的身份证号码;根据进程标识符,用户可以精确地定位一个进程;一个进程标识符唯一对应一个进程,而多个进程标识符可以对应同一个程序。
进程标识符类型: pid_t ,其本质上是一个无符号整型的类型别名(typedef)。

进程与程序的关系: 所谓程序不过是指可运行的二进制代码文件,并且加载到内存中运行就得到了一个进程,同一个程序文件可以被加载多次成为不同的进程。
因此 进程与进程标识符之间是一对一的关系,而与程序文件之间是多对一的关系

WeiyiGeek.进程标识符与进程与程序之间的关系

1.1.1 进程中重要的标识符:
每个进程都有6个重要的ID值,分别是:进程ID、父进程ID、有效用户ID、有效组ID、实际用户ID和实际组ID,这6个ID保存在内核中的数据结构中,有些时候用户程序需要得到这些ID。

例如:在/proc文件系统中,每一个进程都拥有一个子目录,里面存有进程的信息。当使用进程读取这些文件时,应该先得到当前进程的ID才能确定进入哪一个进程的相关子目录。由于这些ID存储在内核之中,因此Linux提供一组专门的接口函数来访问这些ID值。

#include <unistd.h>
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
gid_t getegid(void); //获取有效组ID

//#函数执行成功,返回当前进程的相关ID,执行失败,则返回-1。

执行案例:

/** Linux下的进程标识符 **/

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(void)
{
//进程 process
pid_t pid=0,ppid=0;
//用户 user
uid_t uid=0,euid=0;
//组 group
gid_t gid=0,egid=0;

uid = getuid(); euid = geteuid();
gid = getgid(); egid = getegid();

printf("当前进程ID:%u\t,父进程ID:%u\n",pid,ppid);
printf("用户ID:%u\t,有效用户ID:%u\n",uid,euid);
printf("组ID:%u\t,有效组ID:%u\n",gid,egid);
}

//########################### 执行结果 ##########################################
// [[email protected] ~]# gcc process-demo0.c -o demo0
// [[email protected] ~]# ./demo0
// 当前进程ID:21287 ,父进程ID:18480
// 用户ID:0 ,有效用户ID:0
// 组ID:0 ,有效组ID:0


1.2) 创建进程-fork

Linux系统中最基本的执行单位进程。Linux系统允许任何一个用户创建一个子进程。创建之后,子进程存在于系统之中,并且独立于父进程。
该子进程可以接受系统调度,可以分配到系统资源。系统能检测到它的存在,并且会赋予它与父进程同样的权利。

Linux系统中,使用函数fork()可以创建一个子进程,其函数原型如下:

#include <unistd.h>     /*#包含<unistd.h>*/
#include <sys/types.h> /*#包含<sys/types.h>*/

pid_t fork(void); //pid_t 是一个宏定义,其实质是int 被定义在#include<sys/types.h>中

fork() creates a new process by duplicating the calling process. The new process, referred to as the child, is an exact duplicate of the calling process

fork() 函数会创建一个新的进程,并从内核中为此进程分配一个新的可用的进程标识符(PID),之后,为这个新进程分配进程空间,并将父进程的进程空间中的内容复制到子进程的进程空间中,包括父进程的数据段和堆栈段,并且和父进程共享代码段。这时候系统中又多了一个进程,这个进程和父进程一模一样,两个进程都要接受系统的调度。

返回值:若成功调用一次则返回两个值
(1)在父进程中,fork返回新创建子进程的进程ID;
(2)在子进程中,fork返回0; 由于系统的0号进程是内核进程,所以子进程的进程号不可能是0,由此可以区分父进程和子进程。
(3)如果出现错误,fork返回一个负值。

fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中去,在fork之后处理的文件描述符有两种常见的情况:

  1. 父进程等待子进程完成。父进程无需对其描述符做任何处理。当子进程终止后,子进程对文件偏移量的修改和已执行的更新。
  2. 父子进程各自执行不同的程序段。在fork之后父子进程各自关闭他们不需要使用的文件描述符,这样就不会干扰对方使用文件描述符(网络服务进程中经常使用)。

函数fork()会创建一个新的进程,并从内核中为此进程得到一个新的可用的进程ID,之后为这个新进程分配进程空间,并将父进程的进程空间中的内容复制到子进程的进程空间中,包括父进程的数据段和堆栈段,并且和父进程共享代码段。这时候,系统中又多出一个进程,这个进程和父进程一样,两个进程都要接受系统的调用。

执行案例:

/***
* 进程的创建-fork()
****/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int value = 1023;

int main(void)
{
pid_t pid = fork(); //创建新进程的进程,即调用函数fork()的进程就是父进程。

if(pid < 0)
{
perror("fork Error");
exit(1);
}
else if(pid == 0)
{
value++; //注意点不会受到父进程改变value值的影响, ,如果这里变成 value+=5 ,这是输出的是 value = 1028 = 1023 + 5
printf("Child Process -PID:%u -PPID:%u, (子)创建的第二个进程 %d, Child ID= %d\n\n",getpid(),getppid(),value,pid);
exit(0);
}
else
{
value++; //注意点
printf("Parent Process -PID:%u -PPID:%u, (父)创建的第一个进程 %d,返回子进程PID = %d\n\n",getpid(),getppid(),value,pid);
waitpid(pid,NULL,0); //确保父进程后结束
}

return 0;
}

//########################### 执行结果 ##########################################
// [[email protected] ~]# ./demo1
// Parent Process -PID:21631 -PPID:18480, (父)创建的第一个进程 1024,返回子进程PID = 21632
// Child Process -PID:21632 -PPID:21631, (子)创建的第二个进程 1024, Child ID= 0

//由于创建的新进程和父进程在系统看来是地位平等的两个进程,所以运行机会也是一样的,我们不能够对其执行先后顺序进行假设,先执行哪一个进程取决于系统的调度算法(不同的系统是不同的)。如果想要指定运行的顺序,则需要执行额外的操作。
//正因为如此,程序在运行时并不能保证输出顺序和上面所描述的一致。

Q:在以上的程序中经过多次执行,有时候会出现父进程先执行造成子进程变成孤儿进程?
A: 解决方法:使用 waitpid() 函数,确保父进程后结束;

linux系统提供了函数wait()和waitpid()来回收子进程资源,其函数原型如下:

//#函数说明
#include<sys/types.h>
#include<sys/wait.h>

//#定义函数
pid_t wait(int *statloc);
//参数statloc是一个整型指针。如果statloc不是一个空指针,则终止状态就存放到它所指向的单元内。如果不关心终止状态则将statloc设为空指针。

pid_t waitpid(pid_t pid,int *status,int options); //会暂时停止目前进程的执行,直到有信号来到或子进程结束。
//参数PID:
pid<-1 等待进程组识别码为 pid 绝对值的任何子进程。
pid=-1 等待任何子进程,相当于 wait()。
pid=0 等待进程组识别码与目前进程相同的任何子进程。
pid>0 等待任何子进程识别码为 pid 的子进程。

函数区别:
(1) wait 如果在子进程终止前调用则会阻塞(wait是只要有一个子进程终止就返回);
(2) waitpid 可以使调用者不阻塞并不等待第一个终止的子进程--它有多个选项,可以控制它所等待的进程。

Q:fork 创建的子进程 与 父进程 到底有什么关系呢 ?
A:1.子进程 完全复制 父进程的资源; 2.”写时复制”比如上面的代码中的value++; 3.子进程 与 父进程 占用不同的虚拟内存空间
简单的说:创建一个子进程,子进程完全拥有父进程的资源,但是fork的字符进程有自己的独立空间,也就是当子进程在进行写入等数据改变的时候,操作系统会分派新的空间。

“写时复制”体现: Linux内核实现fork()函数时往往实现了在创建子进程时并不立即复制父进程的数据段和堆栈段,而是当子进程修改这些数据内容时复制才会发生,内核才会给子进程分配进程空间,将父进程的内容复制过来,然后继续后面的操作,为了复制自身完成一些工作的进程来效率会更高。

Q:下列两种情况可能会导致fork()的出错?
A:系统中已经存在了太多的进程;调用函数fork()的用户进程太多(系统中对每个用户所创建的进程数是有限的,如果数量不加限制,那么可以利用这一缺陷恶意攻击系统。)
下面是一个利用进程的特性编写的一个病毒程序,该程序是一个死循环,在循环中不断调用fork()函数来创建子进程,直到系统中不能容纳如此多的进程而崩溃为止。

两个进程都停留在了fork()函数中等待返回(由于在复制时复制了父进程的堆栈段)。因此fork()函数会返回两次(代码有两份,都是从fork函数中返回的),一次是在父进程中返回,另一次是在子进程中返回两次的返回值是不一样的。

WeiyiGeek.fork函数

总结:

  • 除了0号进程以外,任何一个进程都是由其他进程创建的。
  • 进过多次执行,可以发现 子进程号 与 父进程号 是连在一起的,且子进程号 比 父进程号 大1;


1.3) 进程创建-vfork

父子进程的共享资源: 父子进程是两个独立的进程,接受系统调度和分配系统资源的机会均等,子进程完全复制了父进程的地址空间的内容(堆栈段和数据段的内容);此时子进程并没有复制代码段,而是和父进程共用代码段。

因为子进程可能执行不同的流程,那么就会改变数据段和堆栈段,因此需要分开存储父子进程各自的数据段和堆栈段。但是代码段是只读的,不存在被修改的问题,因此这一个段可以让父子进程共享(子进程代码段指向父进程的代码段),以节省存储空间;

vfork()函数:fork()函数类似的函数也可以用来创建一个子进程,只不过新进程与父进程共用父进程的地址空间,其函数原型如下:

#include <unistd.h>
pid_t vfork(void);

(1) vfork()函数产生的子进程和父进程完全共享地址空间,包括代码段、数据段和堆栈段,子进程对这些共享资源所做的修改,可以影响到父进程。vfork()函数与其说是产生了一个进程,还不如说是产生了一个线程。
(2) vfork()函数产生的子进程一定比父进程先运行,也就是说父进程调用了vfork()函数后会等待子进程运行后再运行。

由于父子进程的运行顺序是不确定的,因此我们让父进程休眠了2秒钟以释放CPU控制权(会导致其他线程先运行),子进程先于父进程运行;
vfork 示例:

/**父子进程的共享资源**/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int global = 1; //global variable, stored at data section

int main(void)
{
int stack = 1; //local variable, stored at stack
pid_t pid = vfork();

if(pid < 0)
{
perror("Vfork Error");
exit(1);
}
else if(pid == 0) //子进程会被先执行
{
global++;
stack++;
printf("Child-Process,pid = %u, global=%d, stack=%d\n",getpid(),global,stack);
//execl("/root/demo2","./demo2",NULL); //子进程中另起一个新进程 (会导致循环执行)
4 exit(0); //子进程需手动退出
}
else
{
4 sleep(2); //父进程延迟2秒执行
global++;
stack++;
printf("Parent-Process,fork = %u, global=%d, stack=%d\n",pid,global,stack);
}
return 0;

}
//########################### 执行结果 ##########################################
// [[email protected] ~]# ./demo2
// Child-Process,pid = 7533, global=2, stack=2
// Parent-Process,fork = 7533, global=3, stack=3

Q:vfork 子进程 与 父进程 又有什么关系呢?
A:相似一对亲兄弟享受的母爱是均等的,即共用堆栈段和数据段的内容

Q:vfork 与 fork 的区别与联系:
A:fork与vfork不同的是,建立的父子线程 不能/能 共用堆栈段和数据段的内容。

  • 子进程需要手动退出 (与fork不同)
  • 每次都是子进程先执行, 父进程后执行 (与fork不同)
  • 创建的 子进程 与 父进程 共享空间 (与fork不同)
  • 当然与fork 的相同点是: 都可以创建进程

总结:

  1. vfork创建的子进程在改变了变量之后父进程的资源也被改变了,所以vfork创建的子进程是一种“浅拷贝”。
  2. 先在子进程中修改数据段和堆栈段中的内容,且子进程对这些数据段和堆栈段中内容的修改并不会影响到父进程的进程环境。

子进程继承的资源情况如下表所示:
WeiyiGeek.子进程继承


1.4) 进程终止-exit

进程的退出场景:

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果错误
  • 代码异常中断

进程正常退出的退出方法:

  • 调用exit() : 被认为是温和型退出进程,在进程的任意地方调用都会使得进程退出,在退出的过程中会做 执行用户定义的清理函数,冲刷缓冲区,关闭流等的操作。
  • 调用_exit: 被认为是强制退出进程,在任意地方调用直接退出,不做任何清理工作
  • 调用return : return仅仅是在main函数中才退出进程

Linux退出码查询:echo $?

#include <stdlib.h>
void exit(int status);
void _exit(int status);

exit(0) 函数的参数表示进程的退出状态,这个状态的值是一个整型;
_exit(0) 直接退出

//-----------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
// 0 <= i <133
strerror(i); //返回字符串类型

exit与_exit()区别案例:

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

//exit 与 _exit 区别
int main(void)
{
printf("exit 与 _exit 区别");
exit(0); //在进程结束之前做了清理工作,刷新缓冲区所以才将字符串显示出来
_exit(0); //直接退出不做任何操作
}

//########################### 执行结果 #######################
// [[email protected] ~]# ./demo3
// exit 与 _exit 区别

// 调用_exit() 输出为空

总结:

  • exit() 函数会深入内核注销掉进程的内核数据结构,并且释放掉进程的资源(刷新缓冲区并退出);
  • _exit()函数直接进入内核释放用户进程的地址空间, 所有用户空间的缓冲区内容都将丢失(直接退出)。
  • 其实exit与main函数的return 在底层的实现是调用了_exit;


1.5) 父进程写子进程读

案例代码:

/* 父进程写 子进程读 */
/* waitpid.c 案例应用 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/types.h>

void ChildRead()
{
int fd,ret;
char buf[20] = {0};

fd = open("hello.txt",'r');
if(-1 == fd)
{
perror("ChildRead Error");
exit(2);
}

ret = read(fd,buf,sizeof(buf));
if(-1 == ret)
{
perror("File read Error");
exit(3);
}
else
{
printf("Read from txt:%s\n",buf);
memset(buf,0,sizeof(buf)); //清空缓存区
}

close(fd);
exit(0);
}


void ParentWrite()
{
int fd;
int ret;
char buf[20] = {0};
system("touch hello.txt");
fd = open("hello.txt",O_WRONLY | O_CREAT);
if(-1 == fd)
{
perror("Rarent File open write Error!");
exit(4);
}

printf("Please input the string:\n");
scanf("%s",buf);

ret = write(fd,buf,strlen(buf));
if(-1 == ret)
{
perror("write");
exit(5);
}

close(fd);
}


int main()
{
pid_t pid = fork();
if(-1 == pid)
{
perror("fork Error");
exit(1);
}
else if(0 == pid)
{
sleep(4); //延迟4秒执行
ChildRead();
}
else
{
ParentWrite();
waitpid(pid,NULL,0); //等待子进程结束,父进程回收
}
return 0;
}

//########################### 执行结果 #######################
// [[email protected] ~]# ./demo4
// Please input the string:
// This is demo
// Read from txt:This