进程控制(fork,vfork,exec,exit)

进程标识

每个进程都有一个非负整型表示的唯一进程ID。(虽然是唯一的,但是进程ID是可以复用的,比如当一个进程终止时,其ID就成为复用的候选者,即唯一性是指当前场景下)

ID为0的进程通常是调度进程,常被称为交换进程,属于内核的一部分,其并不执行任何磁盘上的程序,也叫作系统进程。

ID为1的进程通常是init进程,可以通过终端命令 ps aux查看,一般为/sbin/init。init进程绝不会终止,他是一个普通的用户进程(不是系统进程),但是以root权限运行,init是所有孤儿进程的父进程。可参考ps命令的详细信息

除了进程ID,每个进程还有一些其他标识符,可通过以下函数查看

pid_t getpid(void); //获取进程id
pid_t getppid(void); //获取父进程id
uid_t getuid(void); //获取实际使用用户id,程序的执行者id
uid_t geteuid(void); //获取有效用户id,程序的拥有者id
gid_t getgid(void); //获取实际组id
gid_t getegid(void); //获取有效组id

 

父子进程

如果进程B是由进程A开启的,那么我们把进程A叫做进程B的父进程,进程B叫做进程A的子进程。

创建子进程的函数为fork()和vfork()。

fork函数:

pid_t fork(void);

返回值:
    失败返回-1,成功则返回两次(父进程返回子进程id,子进程返回0)
    根据返回值的不同分别为子进程和父进程设计不同的分支
使用:
    1.通过fork()创建的子进程,就是父进程的副本,他会把父进程的堆、栈、全局段、静态数据段、IO流的缓冲区都拷贝一份,父子进程共享代码段。
    2.fork()函数调用成功后,父子进程就开始各自执行,它们的先后顺序是不确定的,但可以通过某些实现来保证(延时)。
    3.当总进程数超过系统限制时,无法创建进程,此时fork()函数会返回失败。
共享:
    父子进程不仅共享代码段,打开的文件也是共享的。
注意:
    fork()执行前,只有父进程在运行,fork之后的代码父子进程都有机会执行,根据fork返回值来控制进入不同的分支。

演示一个有三级子进程的程序:

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

int main(int argc, char const *argv[])
{
	if(fork())//father
	{
		printf("father pid:%d\n",getpid());
		for(;;);
	}

	if(fork())//level 1 son
	{
		printf("son pid1:%d\n my father pid:%d\n",getpid(),getppid());
		for(;;);
	}

	if (fork())//level 2 son
	{
		printf("son pid2:%d\n my father pid:%d\n",getpid(),getppid());
		for(;;);
	}

	if (fork())//level 3 son
	{
		printf("son pid3:%d\n my father pid:%d\n",getpid(),getppid());
		for(;;);
	}

	return 0;
}

fork()创建一个子进程成功时,父进程返回子进程ID,子进程返回0,因此第一个进入if(fork())判断条件的是父进程,同时通过一个死循环让父进程一直待在里面,另一边没有进入第一个if语句的为一级子进程,此时继续使用if(fork())让一级子进程(相较于二级子进程为其父进程)进入,而创建的二级子进程将会跳出第二个if,执行之后的语句,以此类推。

vfork函数:

pid_t vfork(void);

使用:
    1.vfork不能单独创建子进程,需要与excl函数族配合才能完成子进程的创建
    2.它不会复制父进程的栈,堆、数据、全局等段,也不会共享代码段,而是通过excl函数调用一个程序直接启动,从而提高创建进程的效率
    3.vfork()创建子进程保证先执行子进程,在执行父进程

exec函数:

 int execl(const char *path, const char *arg, ...);

path:可执行文件的路径
arg:给可执行文件的参数,类似于命令行参数,必须以NULL结尾,第一个必须是可执行文件名,后续可自由添加
例:excl("path/a.out","a.out",...,NULL);

使用:

1.通过exec创建的子进程会替换掉父进程给的代码段,不拷贝父进程数据,会用新的可执行文件替换掉他们
2.exec只是加载一个可执行文件,并不创建进程,不会产生新的进程号
3.只有exec函数执行结束(无论成功与否),父进程才会继续执行

举个例子,首先我在路径为/home/zhizhen/Os/process/下写一个hello.c,gcc hello.c -o hello 得到可执行文件hello.o

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

int main(int argc, char const *argv[])
{
	for (int i = 0; i < argc; ++i)
	{
		printf("%s\n",argv[i]);
	}
	while(1)
	{
		sleep(1);
		printf("hello world!\n");
	}
	return 0;
}

然后新建一个vfork.c,vfork保证子进程先运行,在它调用exec或exit之后父进程才会恢复运行(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁)。

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

int main(int argc, char const *argv[])
{
	int pid = vfork();
	if (0 == pid)
	{
		printf("son process:%d\n",getpid());
		execl("/home/zhizhen/Os/process/hello","hello","nice to meet you",NULL);
	}

	while(1)
	{
		printf("father process:%d\n",getpid());
		sleep(1);
	}

	return 0;
}

执行结果为:

 

exit函数:

进程的正常退出:
    1.从main退出,在main中执行return 0; (等效于调用exit())
    2. _exit/_Exit(系统/标准C),这两个函数几乎没有区别
               2.1   #include<unistd.h> _exit(int status)
               2.2   #include<stdlib.h>  _Exit(int status)         
                 使用_exit/_Exit退出前会关闭所有打开的文件流,如果有子进程则会托付给init,然后向父进程发送SIGCHLD信号
                 此俩函数没有返回值
    3.exit()
          1.#include<stdlib.h>   void exit(int status);     底层调用_exit/_Exit函数,所有特点它都具备
          2.exit结束前会调用通过atexit/on_exit注册的函数
          3.#include <stdlib.h>
                   int atexit(void (*function)(void));
                   int on_exit(void (*function)(int , void *), void *arg);
    4.最后一个线程正常结束

进程的异常终止:
    1.进程调用abort函数
          1.1段错误
          1.2浮点异常(除0)
   2.进程接受了某些信号
        Ctrl+c
        Ctrl+z
        Ctrl+\
    3.最后一个线程收到取消操作,而且线程收到回应

猜你喜欢

转载自blog.csdn.net/canger_/article/details/81190494