Linux进程知识干货

程堆栈、打开的文件描述符、信号控制设定、 进程优先级、进程组号等。例如,要是父进程打开了五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方。所以,这一步所做的是复制。这样得到的子进程独立于父进程,具有良好的并发性,但是二者之间的通讯需要通过专门的通讯机制。子进程所独有的只有它的进程号、计时器等。使用fork函数的代价是很大的,这些开销并不是所有的情况下都是必须的。比如某进程fork出一个子进程后,其子进程仅仅是为了调用exec执行另一个可执行文件,那么在fork过程中对于虚存空间的复制将是一个多余的过程。但由于现在Linux中是采取了copy-on-write(COW写时复制)技术,为了降低开销,fork最初并不会真的产生两个不同的拷贝,因为在那个时候,大量的数据其实完全是一样的。写时复制是在推迟真正的数据拷贝。若后来确实发生了写入,那意味着parent和child的数据不一致了,于是产生复制动作,每个进程拿到属于自己的那一份,这样就可以降低系统调用的开销。所以有了写时复制后,vfork其实现意义就不大了。 

- 在fork之后,子进程和父进程都会继续执行fork调用之后的指令。子进程是父进程的副本。它将获得父进程的数据空间,堆和栈的副本,这些都是副本,父子进程并不共享这部分的内存。也就是说,子进程对父进程中的同名变量进行修改并不会影响其在父进程中的值。但是父子进程又共享一些东西,简单说来就是程序的正文段。正文段存放着由cpu执行的机器指令,通常是read-only的。 

- 值得注意的是,子进程也是继承父进程的缓冲区的(全缓冲,在填满标准I/O缓冲区后,才进行实际的I/O操作;行缓冲,在遇到换行符时,标准I/O库进行实际I/O 操作。),所以打印输出时在不进行特殊处理的情况下会出现交叉打印。 

- 有些打印函数,如printf,在输出到屏幕上时是行缓冲,但是重定向输出到文件中却变成了全缓冲。比如程序a.out中使用了printf进行输出,如果a.out > log,则printf是按照全缓冲处理的。

vfork函数

定义:pid_t vfork(void); 

vfork和fork的区别:

- vfork保证子进程先运行,父进程会被阻塞直到子进程调用exec或exit之后,父进程才可能被调度运行。

- vfork和fork一样都创建一个子进程,但它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或者exit),于是也就不访问该地址空间。 相反,在子进程调用exec和exit之前,它在父进程的地址空间中运行,在exec之后子进程会有自己的进程地址空间。

注意: 在fork中用return语句是允许的,因为子进程是复制了一份数据。然而,在vfork中用return语句,因为父子进程共享地址空间,子进程return会引起弹栈,导致栈的破坏,也就是父进程不能够继续执行下去了。因此,在vfork中需要用exit()函数或者_exit()函数exec族函数。

clone函数

定义:int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);

系统调用fork()和vfork()是无参数的,而clone()则带有参数。fork()是全部复制,vfork()是共享内存,而clone() 是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的 clone_flags来决定。另外,clone()返回的是子进程的pid。

	CLONE_PARENT  	创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”
	CLONE_FS		子进程与父进程共享相同的文件系统,包括root、当前目录、umask
	CLONE_FILES     子进程与父进程共享相同的文件描述符(file descriptor)表
	CLONE_NEWNS		在新的namespace启动子进程,namespace描述了进程的文件hierarchy
	CLONE_SIGHAND	子进程与父进程共享相同的信号处理(signal handler)表
	CLONE_PTRACE	若父进程被trace,子进程也被trace
	CLONE_VFORK		父进程被挂起,直至子进程释放虚拟内存资源
	CLONE_VM		子进程与父进程运行于相同的内存空间
	CLONE_PID		子进程在创建时PID与父进程一致
	CLONE_THREAD	Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群
复制代码

例子:

  #include <stdio.h>  
  #include <stdlib.h>  
  #include <sched.h>  
  #include <unistd.h>  
  #include <fcntl.h>  
  #include <sys/types.h>  
  #include <sys/stat.h> 
 
  int variable,fd; 
 
  int do_something(){ 
	  variable = 42; 	 
	  printf("in child process\n"); 	 
	  close(fd); 	 
	  return 0;  
  } 
 
  int main(int argc, char *argv[]){ 
	  void *child_stack;  
	  char tempch; 
	 
	  variable = 9; 
	 
	  fd = open("/test.txt",O_RDONLY);  
	  child_stack = (void *)malloc(16384);
	
	  printf("The variable was %d\n",variable); 
	 
	  clone(do_something, child_stack+10000, CLONE_VM |CLONE_FILES,NULL); 
	 
	  sleep(3); /* 延时以便子进程完成关闭文件操作、修改变量 */ 
	 
	  printf("The variable is now %d\n",variable); 
	 
	  if(read(fd,&tempch,1) < 1){ 
		  perror("File Read Error");  
		  exit(1);  
  	} 
 
	  printf("We could read from the file\n");  
	  return 0;  
  } 

  运行结果: 
	  the value was 9  
	  in child process  
	  The variable is now 42  
	  File Read Error
复制代码

2、sleep函数

进程挂起指定的秒数,直到指定的时间用完或者收到信号才解除挂起。注意:进程挂起指定的秒数后程序并不会立即执行,系统只是将此进程切换到了就绪态。

3、wait函数

工作原理:

父进程调用wait函数后阻塞,子进程结束时,系统向其父进程发送SIGCHILD信号。父进程被SIGCHILD信号唤醒然后去回收僵尸子进程,父子进程之间是异步的,SIGCHILD信号机制就是为了解决父子进程之间的异步通信问题,让父进程可以及时的去回收僵尸子进程。若父进程没有任何子进程则wait返回错误。当前进程有可能有多个子进程,wait函数阻塞直到其中一个子进程结束wait就会返回,wait的返回值就可以用来判断到底是哪一个子进程本次被回收了。

	pid_t wait(int *status);
	功能:等待子进程终止,如果终止了,此函数会回收子进程的资源。调用wait函数的进程会挂起,直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒。
		  若调用进程没有子进程或它的子进程已经结束,该函数立即返回。
	参数:函数返回时,参数status中包含子进程退出时的状态信息。子进程的退出信息在一个int中包含了多个字段,用宏定义可以取出其中的每个字段。
	返回值:如果执行成功则返回子进程的进程号,出错返回-1,失败原因存于errno中。
		1、WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。
		2、WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status) 就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说, WIFEXITED返回0,这个值就毫无意义。
复制代码

waitpid函数

	pid_t waitpid(pid_t pid, int *status,int options)
		- pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
		- pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
		- pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
		- pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
		
	options提供了一些额外的选项来控制waitpid,参数option可以为0或可以用"|"运算符把它们连接起来使用,比如:ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);
	如果我们不想使用它们,也可以把options设为0,如:ret=waitpid(-1,NULL,0);
		- WNOHANG 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID。
		- WUNTRACED 若子进程进入暂停状态,则马上返回,但子进程的结束状态不予以理会。WIFSTOPPED(status)宏确定返回值是否对应于一个暂停子进程。
复制代码

 注: wait就是经过包装的waitpid。

	static inline pid_t wait(int *wait_stat)
	{
		return waitpid(-1,wait_stat,0);
	}
复制代码

如果父进程属于守护进程一类,开启TCP套接字等待链接,每当有请求到来,便fork一个子进程传输信息并自由退出。父进程并不关注子进程的退出状态,是否正常都不影响今后的服务,但子进程变成僵尸进程便麻烦了,随着时间的进行,僵尸进程一大堆,虽然占用资源不多,且终究是个隐患。此时可以利用信号处理方式进行非阻塞回收僵死子进程资源。

例子(子进程状态改变会发送SIGCHLD信号给父进程,父进程创建并回收多个子进程):

	#include <stdlib.h>
	#include <stdio.h>
	#include <unistd.h>
	#include <sys/wait.h>
	#include <sys/stat.h>
	#include <sys/mman.h>
	#include <fcntl.h>
	#include <signal.h>

	#define MY_PROCESS_COUNT 10 

	void child_catch(int signalNumber) {
	  //子进程状态发生改变时,内核对信号作处理的回调函数
	  int w_status;
	  pid_t w_pid;

	  while ((w_pid = waitpid(-1, &w_status, WNOHANG)) != -1 && w_pid != 0) {
	    if (WIFEXITED(w_status)) //判断子进程是否正常退出
	      printf("---catch pid %d,return value %d\n", w_pid, WEXITSTATUS(w_status)); //打印子进程PID和子进程返回值
	  }
	}

	int main(int argc, char **argv) {
	  pid_t pid;
	  int i;

	  //在此处阻塞SIGCHLD信号,防止信号处理函数尚未注册成功就有子进程结束
	  sigset_t child_sigset;
	  sigemptyset(&child_sigset); //将child_sigset每一位都设置为0
	  sigaddset(&child_sigset, SIGCHLD); //添加SIGCHLD位
	  sigprocmask(SIG_BLOCK, &child_sigset, NULL); //完成父进程阻塞SIGCHLD的设置 

	  for (i = 0; i < MY_PROCESS_COUNT; i++) {
	    //创建子进程,创建成功就跳出循环
	    if ((pid = fork()) == 0)
	      break;
	  }

	  if (MY_PROCESS_COUNT == i) { //括号内为父进程代码
	    struct sigaction act; //信号回调函数使用的结构体

	    act.sa_handler = child_catch;
	    sigemptyset(&(act.sa_mask)); //设置执行信号回调函数时父进程的的信号屏蔽字
	    act.sa_flags = 0;
	    sigaction(SIGCHLD, &act, NULL); //给SIGCHLD注册信号处理函数

	    //解除SIGCHLD信号的阻塞
	    sigprocmask(SIG_UNBLOCK, &child_sigset, NULL);
	    printf("im PARENT ,my pid is %d\n", getpid());

	    while (1){ //父进程在这里处理自己的相关事情,同时回收僵死子进程资源
				//父进程运行相关代码				
	        }
	  } else {
	    //子进程执行代码
	    printf("im CHILD ,my pid is %d\n", getpid());
	    return i;
	  }
	}
复制代码

4、特殊进程

- 僵尸进程:进程已运行结束,但进程占用的资源未被回收,这样的进程称为僵尸进程。子进程已运行结束,父进程未调用wait或者waitpid函数回收子进程的资源是 子进程变为僵尸进程的原因。

- 孤儿进程:父进程运行结束,但子进程未运行结束的子进程。(在bash终端上,父进程结束后即会释放终端,子进程其实是在后台运行的。)

- 守护进程:守护进程是个特殊的孤儿进程,这种进程脱离终端,在后台运行。

5、exit函数

	#include <stdlib.h>
	void exit(int status)
	参数:status是返回给父进程的参数(低8位有效)。
复制代码

注:exit用于在程序运行的过程中随时结束程序,exit的参数是返回给OS的。main函数结束时也会隐式地调用exit函数。exit函数运行时首先会执行由atexit()函数登记的函数,然后会做一些自身的清理工作,同时刷新所有输出流、关闭所有打开的流并且关闭通过标准I/O函数tmpfile()创建的临时文件。exit是结束一个进程,它将删除进程使用的内存空间,同时把错误信息返回父进程;而return是返回函数值并退出函数。通常情况:exit(0)表示程序正常, exit(1)和exit(-1)表示程序异常退出,exit(2)表示表示系统找不到指定的文件。在整个程序中,只要调用exit就结束(当前进程或者在main时候为整个程序)。

_exit函数

	#include <unistd.h>
	void _exit(int status)
	参数:status是返回给父进程的参数(低8位有效)。
复制代码

注:相比于exit函数,_exit函数是系统调用,而exit函数是库函数。

6、exec函数族

	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 envp[]);
	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[]);
复制代码

exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。 

与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。所以通常我们直接在exec函数调用后直接调用perror()和exit(),无需if判断。

每当有进程认为自己不能为系统和用户做出任何贡献了,他就可以调用任何一个exec,让自己以新的面貌重生;或者,更普遍的情况是,如果一个进程想执行另一个程序,它就可以fork出一个新进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程一样。

事实上第二种情况应用得很普遍,以至于Linux专门为其作了优化,我们知道,fork会将调用进程的所有内容原封不动的拷贝到新产生的子进程中去,这些拷贝的动作很消耗时间,而如果fork完之后我们马上就调用exec,这些辛辛苦苦拷贝来的东西又会被立刻抹掉,这看起来非常不划算,于是人们设计了一种"写时拷贝(copy-on-write)"技术,使得fork结束后并不立刻复制父进程的内容,而是到了真正实用的时候才复制,这样如果下一条语句是exec,它就不会白白作无用功了,也就提高了效率。

	带l的exec函数
		这类函数有:execl,execlp,execle
		具体说明:表示后边的参数以可变参数的形式给出且都以一个空指针结束。这里特别要说明的是,程序名也是参数,所以第一个参数就是程序名。

	带p的exec函数
		这类函数有:execlp,execvp
		具体说明:表示第一个参数无需给出具体的路径,只需给出函数名即可,系统会在PATH环境变量中寻找所对应的程序,如果没找到的话返回-1。

	带v的exec函数
		这类函数有:execv,execvp
		具体说明:表示命令所需的参数以char *arg[]形式给出且arg最后一个元素必须是NULL

	带e的exec函数
		这类函数有:execle
		具体说明:将环境变量传递给需要替换的进程,原来的环境变量不再起作用。
复制代码

事实上,**只有execve是真正的系统调用,其它五个函数最终都调用execve,都是库函数。**一个进程调用exec后,除了进程ID,进程还保留了下列特征不变:父进程号、进程组号、控制终端、根目录、当前工作目录、进程信号屏蔽集、未处理信号,...

附1:

在 Linux 中进程和线程实际上都是用一个结构体 task_struct来表示一个执行任务的实体。进程创建调用fork 系统调用,而线程创建则是 pthread_create 方法,但是这两个方法最终都会调用到 do_fork 来做具体的创建操作 ,区别就在于传入的参数不同。Linux 实现线程的方式很巧妙,实际上根本没有线程,它创建的就是进程,只不过通过参数指定多个进程之间共享某些资源(如虚拟内存、页表、文件描述符等),函数调用栈、寄存器等线程私有数据则独立

但是在其它提供了专门线程支持的系统中,则会在进程控制块(PCB)中增加一个包含指向该进程所有线程的指针,然后再每个线程中再去包含自己独占的资源。这算是非常正统的实现方式了,比如 Windows 就是这样干的。但是相比之下 Linux 就显得取巧很多,也很简洁。


作者:RoundBridge
链接:https://juejin.cn/post/6876681089088389133

猜你喜欢

转载自blog.csdn.net/daocaokafei/article/details/115218868