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

来源:互联网 发布:hm在淘宝没有旗舰店吗 编辑:程序博客网 时间:2024/05/21 11:23

http://21cnbao.blog.51cto.com/109393/120042


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是最难理解的概念之一,它执行一次却返回两个值,完全“不可思议”。先看下面的程序:
{
  if (fork() == 0)
    for (i = 1; i < 3; i++)
  }
  {
      printf("This is parent process\n");
}
执行结果为:
This is child process
This is parent process
fork在英文中是“分叉”的意思,这个名字取得很形象。一个进程在运行中,如果使用了fork,就产生了另一个进程,于是进程就“分叉”了。当前进程为父进程,通过fork()会产生一个子进程。对于父进程,fork函数返回子程序的进程号而对于子程序,fork函数则返回零,这就是一个函数返回两次的本质。可以说,fork函数是Unix系统最杰出的成就之一,它是七十年代Unix早期的开发者经过理论和实践上的长期艰苦探索后取得的成果。
如果我们把上述程序中的循环放的大一点:
{
  if (fork() == 0)
    for (i = 1; i < 10000; i++)
  }
  {
      printf("This is parent process\n");
}
则可以明显地看到父进程和子进程的并发执行,交替地输出“This is child process”和“This is parent process”。
此时此刻,我们还没有完全理解fork()函数,再来看下面的一段程序,看看究竟会产生多少个进程,程序的输出是什么?
{
  for (i = 0; i < 2; i++)
    if (fork() == 0)
      printf("This is child process\n");
    else
      printf("This is parent process\n");
  }
char command[MAX_CMD_LEN];
{
子进程的返回数值  while (1)
    /*  */
    fgets(command, MAX_CMD_LEN, stdin);
    if (fork() == 0)
      /*  */
      /* exec*/
      exit(errorno);
    else
      /*  */
      printf(" child process return %d\n", rtn);
  }
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线程标准,子进程与父进程共享相同的线程群
来看下面的例子:
 
   variable = 42;
   _exit(0);
 
   void **child_stack;
 
   fd = open("test.file", O_RDONLY);
   printf("The variable was %d\n", variable);
   clone(do_something, child_stack, CLONE_VM|CLONE_FILES, NULL);
延时以便子进程完成关闭文件操作、修改变量  
   if (read(fd, &tempch, 1) < 1) {
      exit(1);
   printf("We could read from the file\n");
}
运行输出:
File Read Error
程序的输出结果告诉我们,子进程将文件关闭并将变量修改(调用clone时用到的CLONE_VM、CLONE_FILES标志将使得变量和文件描述符表被共享),父进程随即就感觉到了,这就是clone的特点。
sleep
函数调用sleep可以用来使进程挂起指定的秒数,该函数的原型为:  
void _exit(int status);
_exit会立即终止发出调用的进程,所有属于该进程的文件描述符都关闭。参数status作为退出的状态值返回父进程,在父进程中通过系统调用wait可获得此值。
wait
wait系统调用包括:
pid_t waitpid(pid_t pid, int *status, int options);
wait的作用为发出调用的进程只要有子进程,就睡眠到它们中的一个终止为止; waitpid等待由参数pid指定的子进程退出。
3.进程间通信
Linux的进程间通信(IPC,InterProcess Communication)通信方法有管道、消息队列、共享内存、信号量、套接口等。
管道分为有名管道和无名管道,无名管道只能用于亲属进程之间的通信,而有名管道则可用于无亲属关系的进程之间。
#define OUTPUT 1
{
  /* */
  char buf[BUFFER_LEN];
  /**/
  /**/
  {
    exit(1);
  /**/
  {
    /**/
    write(file_descriptors[OUTPUT], "test data", strlen("test data"));
  }
  {
执行父进程    printf("in the spawning (parent) process...\n");
父进程从管道读取子进程写的数据,关闭管道的写端    close(file_descriptors[OUTPUT]);
    printf("%d bytes of data received from spawned process: %s\n",
  }
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;
  char buf[BUFFER_LEN];
  if (in_file == NULL)
    printf("Error in fdopen.\n");
  }
    printf("received from pipe: %s\n", buf);
}
/* */
{
  int count = 1;
  out_file = fopen("pipeexample", "w");
  {
    exit(1);
  sprintf(buf, "this is test data for the named pipe example\n");
  fclose(out_file);
int shmget(key_t key, int size, int flag);获得一个共享存储标识符void *shmat(int shmid, void *addr, int flag); /* */
shmid为shmget函数返回的共享存储标识符,addr和flag参数决定了以什么方式来确定连接的地址,函数的返回值即是该进程数据段所连接的实际地址。此后,进程可以对此地址进行读写操作访问共享内存。
本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。一般说来,为了获得共享资源,进程需要执行下列操作:
(1)测试控制该资源的信号量;
(2)若此信号量的值为正,则允许进行使用该资源,进程将进号量减1;
(3)若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤(1);
(4)当进程不再使用一个信号量控制的资源时,信号量值加1,如果此时有进程正在睡眠等待此信号量,则唤醒此进程。
下面是一个使用信号量的例子,该程序创建一个特定的IPC结构的关键字和一个信号量,建立此信号量的索引,修改索引指向的信号量的值,最后清除信号量:
#include <sys/types.h>
#include <sys/ipc.h>
{
定义一个关键字  int id;
  union semun options;
 
生成关键字,字符是一个随机种子  /* */
  printf("semaphore id=%d\n", id);
设置变量值  semctl(id, 0, SETVAL, options); /*0*/
  /**/
  printf("value of semaphore at index 0 is %d\n", i);
  /**/
设置哪个信号量  lock_it.sem_op =  - 1; /**/
操作方式  if (semop(id, &lock_it, 1) ==  - 1)
    printf("can not lock semaphore.\n");
  }
  i = semctl(id, 0, GETVAL, 0);
 
清除信号量  semctl(id, 0, IPC_RMID, 0);
<span lang="EN-US" )="" 0%="" 50%;="" -moz-background-clip:="" -moz-initial;="" -moz-background-origin:="" -moz-background-inline-policy:="" -moz-initial"="" style="padding: 0px; margin: 0px;">}
套接字通信并不为Linux所专有,在所有提供了TCP/IP协议栈的操作系统中几乎都提供了socket,而所有这样操作系统,对套接字的编程方法几乎是完全一样的。
4.小节
本章讲述了Linux进程的概念,并以多个实例讲解了进程控制及进程间通信方法,理解这一章的内容可以说是理解Linux这个操作系统的关键。

原创粉丝点击