深入理解计算机系统 ---进程控制

本文详细介绍了Unix/Linux系统中进程管理的相关函数和概念,包括获取进程信息(getpid, getppid)、创建和终止进程(fork, exit, _exit)、回收子进程(wait, waitpid)以及让进程休眠(sleep, pause)。通过示例代码展示了如何使用这些函数,解释了它们的工作原理和使用场景,如进程的独立地址空间、同步与异步回收、进程状态等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1.获取进程信息:

2.创建和终止进程

3.回收子进程

4.让进程休眠

5.加载并运行程序 、利用fork和execve运行程序


1.获取进程信息

pid_t getpid(void)  //返回当前进程的 PID
pid_t getppid(void) //返回当前进程的父进程的 PID

2.创建和终止进程

终止进程:

  1. 接收到一个终止信号
  2. 返回到 main
  3. 调用了 exit 函数

exit 函数:

// 以 status 状态终止进程
void exit(int status)
  • _exit() :退出程序;
  • exit(0):运行正常退出程序;
  • exit(1):运行异常退出程序;
  • return():返回函数,若在主函数中,则会退出函数并返回值。

 创建进程:

调用 fork 来创造新进程。这个函数很有趣,执行一次,但是会返回两次,具体的函数原型为

// 对于子进程,返回 0
// 对于父进程,返回子进程的 PID
int fork(void)

返回值:

RETURN VALUE
On  success,  
the  PID  of  the  child  process is returned in the parent, 
and 0 is returned in the child.  
On failure, -1 is returned in the parent, no child process is created, 
and errno is set appropriately.

 子进程几乎和父进程一模一样,会有相同且独立的虚拟地址空间,也会得到父进程已经打开的文件描述符(file descriptor)。比较明显的不同之处就是进程 PID 了。按照手册说法:

The child process and the parent process run in separate memory spaces.
At the time of fork() both memory  spaces  have  the same  content.
Memory  writes, file mappings (mmap(2)), and unmappings (munmap(2)) performed by one of the processes do not affect the other.

也就是说:父进程和子进程在不同的内存中运行,但是却有相同的内容 ,如下示例中 int x 就是两者共用的数据。

程序:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
/* $begin fork */
/* $begin wasidefork */
int main() 
{
  pid_t pid;
  int x = 1;

	//在父进程中创建一个子进程
  pid = fork();
	
  if(pid == 0) 
	{  /* Child */
		printf("child : x=%d\n", ++x); //line:ecf:childprint
		exit(0);
  }

    /* Parent */
    printf("parent: x=%d\n", --x); //line:ecf:parentprint
    exit(0);
}
/* $end fork */
/* $end wasidefork */

输出:

parent: x=0
child : x=2

运行结果说明: 

  • 调用一次,但是会有两个返回值
  • 并行执行,不能预计父进程和子进程的执行顺序(在本系统中,父进程先完成printf语句,然后是子进程)
  • 拥有自己独立的地址空间(也就是变量都是独立的),除此之外其他都相同
  • 在父进程和子进程中 stdout 是一样的(都会发送到标准输出)

进程图:

3.回收子进程

wait函数

pid_t wait(int *status);

wait的参数status。status用来返回子进程结束时的状态,父进程通过wait得到status后就可以知道子进程的一些结束状态信息。wait的返回值pid_t,这个返回值就是本次wait回收的子进程的PID。当前进程有可能有多个子进程,wait函数阻塞直到其中一个子进程结束wait就会返回,wait的返回值就可以用来判断到底是哪一个子进程本次被回收了。总结:wait主要是用来回收子进程资源,回收同时还可以得知被回收子进程的pid和退出状态。

WIFEXITED、WIFSIGNALED、WEXITSTATUS 这几个宏用来获取子进程的退出状态。

WIFEXITED 宏用来判断子进程是否正常终止(return、exit、_exit退出)

WIFSIGNALED 宏用来判断子进程是否非正常终止(被信号所终止)

WEXITSTATUS 宏用来得到正常终止情况下的进程返回值的。

使用waitpid实现wait的效果

ret = waitpid(-1, &status, 0);    

 -1 表示不等待某个特定PID的子进程而是回收任意一个子进程,0表示用默认的方式(阻塞式)来进行等待,返回值ret是本次回收的子进程的PID。当调用进程没有子进程,那么waitpid返回-1,并设置reeno为EINTR。

ret = waitpid(pid, &status, 0);

等待回收PID为pid的这个子进程,如果当前进程并没有一个ID号为pid的子进程,则返回值为-1;如果成功回收了pid这个子进程则返回值为回收的进程的PID

ret = waitpid(pid, &status, WNOHANG);

这种表示父进程要非阻塞式的回收子进程。

如果父进程执行waitpid时子进程已经先结束等待回收则waitpid直接回收成功,返回值是回收的子进程的PID;如果父进程waitpid时子进程尚未结束则父进程立刻返回(非阻塞),但是返回值为0(表示回收不成功)。

程序:

/* $begin waitpid1 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>

#define N 10

int main() 
{
    int status, i;
    pid_t pid;

	//在父进程中创建N个子进程
    for (i = 0; i < N; i++)                      
		if ((pid = fork()) == 0)  /* Child */     
	    exit(100+i);               //每个子进程分别以唯一的退出状态退出           

    //父进程中用while循环,调用 waitpid 等待所有进程终止
	//当回收了所有进程后,再调用waitpid返回就是-1,并且设置 errno = ECHILD
    while ((pid = waitpid(-1, &status, 0)) > 0) 
	{ 
		if (WIFEXITED(status))  //判断子进程是否正常终止,exit
			printf("child %d terminated normally with exit status=%d\n",
					pid, WEXITSTATUS(status));     
		else
	    	printf("child %d terminated abnormally\n", pid);
    }

    /* The only normal termination is if there are no more children */
    if (errno != ECHILD)                          //line:ecf:waitpid1:errno
	printf("waitpid error");

    exit(0);
}
/* $end waitpid1 */

4.让进程休眠

//将一个进程挂起一段指定的时间
unsigned int sleep(unsigned int secs); 

//让调用函数休眠,直到该进程收到一个信号
int pause(void); 

5.加载并运行程序 、利用fork和execve运行程序

这里只举个简单的:

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

execl() 其中后缀 " l " 代表 list 也就是参数列表的意思 

第一参数 path 字符指针所指向要执行的文件路径 

之后的 arg 参数代表执行该文件时传递的参数列表 :argv[0] , argv[1] ...

最后一个参数须用空指针NULL作结束 

为什么需要exec函数?
(1)   fork子进程是为了执行新程序(fork创建了子进程后,子进程和父进程同时被OS调度执行,因此子进程可以单独的执行一个程序,这个程序宏观上将会和父进程程序同时进行)
(2)   可以直接在子进程的 if 中写入新程序的代码。这样可以,但是不够灵活,因为我们只能把子进程程序的源代码贴过来执行(必须知道源代码,而且源代码太长了也不好控制),譬如说我们希望子进程来执行ls -la 命令就不行了(没有源代码,只有编译好的可执行程序)
(3)   使用exec族运行新的可执行程序(exec族函数可以直接把一个编译好的可执行程序直接加载运行)
(4)   我们有了exec族函数后,我们典型的父子进程程序是这样的:子进程需要运行的程序被单独编写、单独编译连接成一个可执行程序(叫hello),(项目是一个多进程项目)主程序为父进程,fork创建了子进程后在子进程中exec来执行hello,达到父子进程分别做不同程序同时(宏观上)运行的效果。

下面举一个简单的例子:

在子进程中执行 :

ls -l -a
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>  
#include <sys/wait.h>
#include <stdlib.h>

int main(void)
{
	pid_t pid = -1;
	pid_t ret = -1;
	int status = -1;

	pid = fork();

	if (pid > 0)
	{
		// 父进程
		printf("parent, 子进程id = %d.\n", pid);
	}
	else if (pid == 0)
	{
		// 子进程
		execl("/bin/ls", "ls", "-l", "-a", NULL);		// ls -l -a
		return 0;
	}
	else
	{
		perror("fork");
		return -1;
	}
	return 0;
}

输出:

总用量 28
drwxrwxr-x  2 wdd wdd 4096 9月  14 00:20 .
drwxr-xr-x 34 wdd wdd 4096 9月  13 17:05 ..
-rwxrwxr-x  1 wdd wdd 8760 9月  14 00:16 a.out
-rwxrw-rw-  1 wdd wdd  486 9月  14 00:15 execl.c
-rwxrw-rw-  1 wdd wdd  163 9月  14 00:20 hello.c

如果我们先编译好一个可执行文件,程序如下:

#include <stdio.h>

int main(int argc, char **argv)
{
	int i = 0;
	printf("argc = %d.\n", argc);

	while (NULL != argv[i])
	{
		printf("argv[%d] = %s\n", i, argv[i]);
		i++;
	}
	return 0;
}

然后用gcc生成可执行文件:

把上面的: execl("/bin/ls", "ls", "-l", "-a", NULL);  换成:

execl("hello", "aaa", "bbb", NULL);

 结果如下输出如下:

argc = 2.
argv[0] = aaa
argv[1] = bbb

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值