Linux下的C编程实战(三)――进程控制与进程通信编程

简介:
Linux 下的 C 编程实战(三)

――进程控制与进程通信编程

作者: 宋宝华  e-mail: [email]21cnbao@21cn.com[/email]

1.Linux 进程

       Linux 进程在内存中包含三部分数据:代码段、堆栈段和数据段。代码段存放了程序的代码。代码段可以为机器中运行同一程序的数个进程共享。堆栈段存放的是子程序(函数)的返回地址、子程序的参数及程序的局部变量。而数据段则存放程序的全局变量、常数以及动态数据分配的数据空间(比如用 malloc 函数申请的内存)。与代码段不同,如果系统中同时运行多个相同的程序,它们不能使用同一堆栈段和数据段。
Linux 进程主要有如下几种状态:用户状态(进程在用户状态下运行的状态)、内核状态(进程在内核状态下运行的状态)、内存中就绪(进程没有执行,但处于就绪状态,只要内核调度它,就可以执行)、内存中睡眠(进程正在睡眠并且处于内存中,没有被交换到 SWAP 设备)、就绪且换出(进程处于就绪状态,但是必须把它换入内存,内核才能再次调度它进行运行)、睡眠且换出(进程正在睡眠,且被换出内存)、被抢先(进程从内核状态返回用户状态时,内核抢先于它,做了上下文切换,调度了另一个进程,原先这个进程就处于被抢先状态)、创建状态(进程刚被创建,该进程存在,但既不是就绪状态,也不是睡眠状态,这个状态是除了进程 0 以外的所有进程的最初状态)、僵死状态(进程调用 exit 结束,进程不再存在,但在进程表项中仍有记录,该记录可由父进程收集)。
下面我们来以一个进程从创建到消亡的过程讲解 Linux 进程状态转换的“生死因果”。
1 )进程被父进程通过系统调用 fork 创建而处于创建态;
2 fork 调用为子进程配置好内核数据结构和子进程私有数据结构后,子进程进入就绪态(或者在内存中就绪,或者因为内存不够而在 SWAP 设备中就绪);
3 )若进程在内存中就绪,进程可以被内核调度程序调度到 CPU 运行;
4 )内核调度该进程进入内核状态,再由内核状态返回用户状态执行。该进程在用户状态运行一定时间后,又会被调度程序所调度而进入内核状态,由此转入就绪态。有时进程在用户状态运行时,也会因为需要内核服务,使用系统调用而进入内核状态,服务完毕,会由内核状态转回用户状态。要注意的是,进程在从内核状态向用户状态返回时可能被抢占,这是由于有优先级更高的进程急需使用 CPU ,不能等到下一次调度时机,从而造成抢占;
5 )进程执行 exit 调用,进入僵死状态,最终结束。
2. 进程控制

进程控制中主要涉及到进程的创建、睡眠和退出等,在 Linux 中主要提供了 fork exec clone 的进程创建方法, sleep 的进程睡眠和 exit 的进程退出调用,另外 Linux 还提供了父进程等待子进程结束的系统调用 wait
fork

对于没有接触过 Unix/Linux 操作系统的人来说, fork 是最难理解的概念之一,它执行一次却返回两个值,完全“不可思议”。先看下面的程序:
int main()

{

  int i;

  if (fork() == 0)

  {

    for (i = 1; i < 3; i++)

      printf("This is child process\n");

  }

  else

  {

    for (i = 1; i < 3; i++)

      printf("This is parent process\n");

  }

}

执行结果为:
This is child process

This is child process

This is parent process

This is parent process

fork 在英文中是“分叉”的意思,这个名字取得很形象。一个进程在运行中,如果使用了 fork ,就产生了另一个进程,于是进程就“分叉”了。当前进程为父进程,通过 fork() 会产生一个子进程。对于父进程, fork 函数返回子程序的进程号而对于子程序, fork 函数则返回零,这就是一个函数返回两次的本质。可以说, fork 函数是 Unix 系统最杰出的成就之一,它是七十年代 Unix 早期的开发者经过理论和实践上的长期艰苦探索后取得的成果。
如果我们把上述程序中的循环放的大一点:
int main()

{

  int i;

  if (fork() == 0)

  {

    for (i = 1; i < 10000; i++)

      printf("This is child process\n");

  }

  else

  {

    for (i = 1; i < 10000; i++)

      printf("This is parent process\n");

  }

}

则可以明显地看到父进程和子进程的并发执行,交替地输出“ This is child process ”和“ This is parent process ”。
此时此刻,我们还没有完全理解 fork() 函数,再来看下面的一段程序,看看究竟会产生多少个进程,程序的输出是什么?
int main()

{

  int i;

  for (i = 0; i < 2; i++)

  {

    if (fork() == 0)

    {

      printf("This is child process\n");

    }

    else

    {

      printf("This is parent process\n");

    }

  }

}

exec

Linux 中可使用 exec 函数族,包含多个函数( execl execlp execle execv execve execvp ),被用于启动一个指定路径和文件名的进程。
exec 函数族的特点体现在:某进程一旦调用了 exec 类函数,正在执行的程序就被干掉了,系统把代码段替换成新的程序(由 exec 类函数执行)的代码,并且原有的数据段和堆栈段也被废弃,新的数据段与堆栈段被分配,但是进程号却被保留。也就是说, exec 执行的结果为:系统认为正在执行的还是原先的进程,但是进程对应的程序被替换了。
fork 函数可以创建一个子进程而当前进程不死,如果我们在 fork 的子进程中调用 exec 函数族就可以实现既让父进程的代码执行又启动一个新的指定进程,这实在是很妙的。 fork exec 的搭配巧妙地解决了程序启动另一程序的执行但自己仍继续运行的问题,请看下面的例子:
char command[MAX_CMD_LEN];

void main()

{

  int rtn; /*  子进程的返回数值  */

  while (1)

  {

    /*  从终端读取要执行的命令  */

    printf(">");

    fgets(command, MAX_CMD_LEN, stdin);

    command[strlen(command) - 1] = 0;

    if (fork() == 0)

    {

      /*  子进程执行此命令  */

      execlp(command, command);

      /*  如果 exec 函数返回,表明没有正常执行命令,打印错误信息 */

      perror(command);

      exit(errorno);

    }

    else

    {

      /*  父进程,等待子进程结束,并打印子进程的返回值  */

      wait(&rtn);

      printf(" child process return %d\n", rtn);

    }

  }

}

这个函数基本上实现了一个 shell 的功能,它读取用户输入的进程名和参数,并启动对应的进程。
clone

clone Linux2.0 以后才具备的新功能,它较 fork 更强(可认为 fork clone 要实现的一部分),可以使得创建的子进程共享父进程的资源,并且要使用此函数必须在编译内核时设置 clone_actually_works_ok 选项。
clone 函数的原型为:
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);

此函数返回创建进程的 PID ,函数中的 flags 标志用于设置创建子进程时的相关选项,具体含义如下表:
标志

含义

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 线程标准,子进程与父进程共享相同的线程群

来看下面的例子:
int variable, fd;

 

int do_something() {

   variable = 42;

   close(fd);

   _exit(0);

}

 

int main(int argc, char *argv[]) {

   void **child_stack;

   char tempch;

 

   variable = 9;

   fd = open("test.file", O_RDONLY);

   child_stack = (void **) malloc(16384);

   printf("The variable was %d\n", variable);

  

   clone(do_something, child_stack, CLONE_VM|CLONE_FILES, NULL);

   sleep(1);   /*  延时以便子进程完成关闭文件操作、修改变量  */

 

   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 variable is now 42

File Read Error

程序的输出结果告诉我们,子进程将文件关闭并将变量修改(调用 clone 时用到的 CLONE_VM CLONE_FILES 标志将使得变量和文件描述符表被共享),父进程随即就感觉到了,这就是 clone 的特点。
sleep

函数调用 sleep 可以用来使进程挂起指定的秒数,该函数的原型为:  
unsigned int sleep(unsigned int seconds);

该函数调用使得进程挂起一个指定的时间,如果指定挂起的时间到了,该调用返回 0 ;如果该函数调用被信号所打断,则返回剩余挂起的时间数(指定的时间减去已经挂起的时间)。
exit

系统调用 exit 的功能是终止本进程,其函数原型为:
void _exit(int status);

_exit 会立即终止发出调用的进程,所有属于该进程的文件描述符都关闭。参数 status 作为退出的状态值返回父进程,在父进程中通过系统调用 wait 可获得此值。
wait

wait 系统调用包括:
pid_t wait(int *status);

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

wait 的作用为发出调用的进程只要有子进程,就睡眠到它们中的一个终止为止;  waitpid 等待由参数 pid 指定的子进程退出。
3. 进程间通信

Linux 的进程间通信( IPC InterProcess Communication )通信方法有管道、消息队列、共享内存、信号量、套接口等。
管道分为有名管道和无名管道,无名管道只能用于亲属进程之间的通信,而有名管道则可用于无亲属关系的进程之间。
#define INPUT 0

#define OUTPUT 1

void main()

{

  int file_descriptors[2];

  /* 定义子进程号  */

  pid_t pid;

  char buf[BUFFER_LEN];

  int returned_count;

  /* 创建无名管道 */

  pipe(file_descriptors);

  /* 创建子进程 */

  if ((pid = fork()) ==  - 1)

  {

    printf("Error in fork\n");

    exit(1);

  }

  /* 执行子进程 */

  if (pid == 0)

  {

    printf("in the spawned (child) process...\n");

    /* 子进程向父进程写数据,关闭管道的读端 */

    close(file_descriptors[INPUT]);

    write(file_descriptors[OUTPUT], "test data", strlen("test data"));

    exit(0);

  }

  else

  {

    /* 执行父进程 */

    printf("in the spawning (parent) process...\n");

    /* 父进程从管道读取子进程写的数据,关闭管道的写端 */

    close(file_descriptors[OUTPUT]);

    returned_count = read(file_descriptors[INPUT], buf, sizeof(buf));

    printf("%d bytes of data received from spawned process: %s\n",

      returned_count, buf);

  }

}

上述程序中,无名管道以
int pipe(int filedis[2]);

方式定义,参数 filedis 返回两个文件描述符 filedes[0] 为读而打开, filedes[1] 为写而打开, filedes[1] 的输出是 filedes[0] 的输入;
Linux 系统下,有名管道可由两种方式创建(假设创建一个名为“ fifoexample ”的有名管道):
1 mkfifo("fifoexample","rw");
2 mknod fifoexample p
mkfifo 是一个函数, mknod 是一个系统调用,即我们可以在 shell 下输出上述命令。
有名管道创建后,我们可以像读写文件一样读写之:
/*  进程一:读有名管道 */

void main()

{

  FILE *in_file;

  int count = 1;

  char buf[BUFFER_LEN];

  in_file = fopen("pipeexample", "r");

  if (in_file == NULL)

  {

    printf("Error in fdopen.\n");

    exit(1);

  }

  while ((count = fread(buf, 1, BUFFER_LEN, in_file)) > 0)

    printf("received from pipe: %s\n", buf);

  fclose(in_file);

}

 

/*  进程二:写有名管道 */

void main()

{

  FILE *out_file;

  int count = 1;

  char buf[BUFFER_LEN];

  out_file = fopen("pipeexample", "w");

  if (out_file == NULL)

  {

    printf("Error opening pipe.");

    exit(1);

  }

  sprintf(buf, "this is test data for the named pipe example\n");

  fwrite(buf, 1, BUFFER_LEN, out_file);

  fclose(out_file);

}

消息队列用于运行于同一台机器上的进程间通信,与管道相似;
共享内存通常由一个进程创建,其余进程对这块内存区进行读写。得到共享内存有两种方式:映射 /dev/mem 设备和内存映像文件。前一种方式不给系统带来额外的开销,但在现实中并不常用,因为它控制存取的是实际的物理内存;常用的方式是通过 shmXXX 函数族来实现共享内存:
int shmget(key_t key, int size, int flag);   /*  获得一个共享存储标识符  */

该函数使得系统分配 size 大小的内存用作共享内存;
void *shmat(int shmid, void *addr, int flag); /*  将共享内存连接到自身地址空间中 */

shmid shmget 函数返回的共享存储标识符, addr flag 参数决定了以什么方式来确定连接的地址,函数的返回值即是该进程数据段所连接的实际地址。此后,进程可以对此地址进行读写操作访问共享内存。
本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。一般说来,为了获得共享资源,进程需要执行下列操作:
1 )测试控制该资源的信号量;
2 )若此信号量的值为正,则允许进行使用该资源,进程将进号量减 1
3 )若此信号量为 0 ,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于 0 ,进程被唤醒,转入步骤( 1 );
4 )当进程不再使用一个信号量控制的资源时,信号量值加 1 ,如果此时有进程正在睡眠等待此信号量,则唤醒此进程。
下面是一个使用信号量的例子,该程序创建一个特定的 IPC 结构的关键字和一个信号量,建立此信号量的索引,修改索引指向的信号量的值,最后清除信号量:
#include <stdio.h>

#include <sys/types.h>

#include <sys/sem.h>

#include <sys/ipc.h>

void main()

{

  key_t unique_key; /*  定义一个 IPC 关键字 */

  int id;

  struct sembuf lock_it;

  union semun options;

  int i;

 

  unique_key = ftok(".", 'a'); /*  生成关键字,字符 'a' 是一个随机种子 */

  /*  创建一个新的信号量集合 */

  id = semget(unique_key, 1, IPC_CREAT | IPC_EXCL | 0666);

  printf("semaphore id=%d\n", id);

  options.val = 1; /* 设置变量值 */

  semctl(id, 0, SETVAL, options); /* 设置索引 0 的信号量 */

 

  /* 打印出信号量的值 */

  i = semctl(id, 0, GETVAL, 0);

  printf("value of semaphore at index 0 is %d\n", i);

 

  /* 下面重新设置信号量 */

  lock_it.sem_num = 0; /* 设置哪个信号量 */

  lock_it.sem_op =  - 1; /* 定义操作 */

  lock_it.sem_flg = IPC_NOWAIT; /* 操作方式 */

  if (semop(id, &lock_it, 1) ==  - 1)

  {

    printf("can not lock semaphore.\n");

    exit(1);

  }

 

  i = semctl(id, 0, GETVAL, 0);

  printf("value of semaphore at index 0 is %d\n", i);

 

  /* 清除信号量 */

  semctl(id, 0, IPC_RMID, 0);

}

套接字通信并不为 Linux 所专有,在所有提供了 TCP/IP 协议栈的操作系统中几乎都提供了 socket ,而所有这样操作系统,对套接字的编程方法几乎是完全一样的。
4. 小节

本章讲述了 Linux 进程的概念,并以多个实例讲解了进程控制及进程间通信方法,理解这一章的内容可以说是理解 Linux 这个操作系统的关键。


 本文转自 21cnbao 51CTO博客,原文链接:http://blog.51cto.com/21cnbao/120042,如需转载请自行联系原作者



相关文章
|
7天前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
|
6天前
|
存储 算法 Linux
【实战项目】网络编程:在Linux环境下基于opencv和socket的人脸识别系统--C++实现
【实战项目】网络编程:在Linux环境下基于opencv和socket的人脸识别系统--C++实现
20 6
|
6天前
|
算法 Linux 调度
深度解析:Linux内核的进程调度机制
【4月更文挑战第12天】 在多任务操作系统如Linux中,进程调度机制是系统的核心组成部分之一,它决定了处理器资源如何分配给多个竞争的进程。本文深入探讨了Linux内核中的进程调度策略和相关算法,包括其设计哲学、实现原理及对系统性能的影响。通过分析进程调度器的工作原理,我们能够理解操作系统如何平衡效率、公平性和响应性,进而优化系统表现和用户体验。
15 3
|
11天前
|
监控 Linux Shell
初识Linux下进程2
初识Linux下进程2
|
11天前
|
Linux 编译器 Windows
【Linux】10. 进程地址空间
【Linux】10. 进程地址空间
19 4
|
15天前
|
Web App开发 人工智能 Ubuntu
【Linux】Linux启动/查看/结束进程命令(详细讲解)
【Linux】Linux启动/查看/结束进程命令(详细讲解)
|
16天前
|
传感器 Linux API
嵌入式Linux串口编程简介
嵌入式Linux串口编程简介
14 1
|
16天前
|
Linux 测试技术 C语言
【Linux】应用编程之C语言文件操作
【Linux】应用编程之C语言文件操作
|
21天前
|
Linux Shell 调度
【Linux】进程排队的理解&&进程状态的表述&&僵尸进程和孤儿进程的理解
【Linux】进程排队的理解&&进程状态的表述&&僵尸进程和孤儿进程的理解
|
22天前
|
监控 Linux Shell
Linux 进程问题调查探秘:分析和排查频繁创建进程问题
Linux 进程问题调查探秘:分析和排查频繁创建进程问题
39 0