当前位置:首页 > 资讯 > info5 > 正文

Linux下的C编程入门之进程控制与进程通信编程

发表于: 2010-04-13   作者:yangdelong   来源:转载   浏览次数:
摘要: 本文来自:Linux教程--http://doc.linuxpk.com/53277.html如有不明白之处,欢迎参加社区讨论1.Linux进程 Linux进程在内存中包含三部分数据:代码段、堆栈段和数据段。代码段存放了程序的代码。代码段可以为机器中运行同一程序的数个进程共享。堆栈段存放的是子程序(函数)的返回地址、子程序的参数及程序的局部变量。而数据段则存放程序的全局变量、常数以及动态数据分配的

本文来自:Linux教程 -- http://doc.linuxpk.com/53277.html
如有不明白之处,欢迎参加社区讨论

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这个操作系统的关键。

  

本文来自:Linux教程 -- http://doc.linuxpk.com/53277.html
如有不明白之处,欢迎参加社区讨论
fork的精辟剖析
http://blog.chinaunix.net/u2/62910/showart_492770.html

论坛上高手对fork的精辟剖析。


程序如下:

 

#include <unistd.h>;

#include <sys/types.h>;

main ()

{

   pid_t pid;

        pid=fork();

        if (pid < 0)

                printf("error in fork!");

        else if (pid == 0)

                printf("i am the child process, my process id is %dn",getpid());

        else

                printf("i am the parent process, my process id is %dn",getpid());

 }

结果是

[root@localhost c]# ./a.out

i am the child process, my process id is 4286

i am the parent process, my process id is 4285

 

高手一:

要搞清楚fork的执行过程,就必须先讲清楚操作系统中的“进程(process)”概念。一个进程,主要包含三个元素:

o. 一个可以执行的程序;

o. 和该进程相关联的全部数据(包括变量,内存空间,缓冲区等等);

o. 程序的执行上下文(execution context)。

不妨简单理解为,一个进程表示的,就是一个可执行程序的一次执行过程中的一个状态。操作系统对进程的管理,典型的情况,是通过进程表完成的。进程表中的每一个表项,记录的是当前操作系统中一个进程的情况。对于单 CPU的情况而言,每一特定时刻只有一个进程占用 CPU,但是系统中可能同时存在多个活动的(等待执行或继续执行的)进程。一个称为“程序计数器(program counter,  pc)”的寄存器,指出当前占用 CPU的进程要执行的下一条指令的位置。当分给某个进程的 CPU时间已经用完,操作系统将该进程相关的寄存器的值,保存到该进程在进程表中对应的表项里面;把将要接替这个进程占用 CPU的那个进程的上下文,从进程表中读出,并更新相应的寄存器(这个过程称为“上下文交换(process context switch)”,实际的上下文交换需要涉及到更多的数据,那和fork无关,不再多说,主要要记住程序寄存器 pc指出程序当前已经执行到哪里,是进程上下文的重要内容,换出 CPU的进程要保存这个寄存器的值,换入CPU的进程,也要根据进程表中保存的本进程执行上下文信息,更新这个寄存器)。

好了,有这些概念打底,可以说fork了。当你的程序执行到下面的语句:

pid=fork(); 

操作系统创建一个新的进程(子进程),并且在进程表中相应为它建立一个新的表项。新进程和原有进程的可执行程序是同一个程序;上下文和数据,绝大部分就是原进程(父进程)的拷贝,但它们是两个相互独立的进程!此时程序寄存器pc,在父、子进程的上下文中都声称,这个进程目前执行到fork调用即将返回(此时子进程不占有CPU,子进程的pc不是真正保存在寄存器中,而是作为进程上下文保存在进程表中的对应表项内)。问题是怎么返回,在父子进程中就分道扬镳。

父进程继续执行,操作系统对fork的实现,使这个调用在父进程中返回刚刚创建的子进程的pid(一个正整数),所以下面的if语句中pid<0, pid==0的两个分支都不会执行。所以输出i am the parent process...

子进程在之后的某个时候得到调度,它的上下文被换入,占据 CPU,操作系统对fork的实现,使得子进程中fork调用返回0。所以在这个进程(注意这不是父进程了哦,虽然是同一个程序,但是这是同一个程序的另外一次执行,在操作系统中这次执行是由另外一个进程表示的,从执行的角度说和父进程相互独立)中pid=0。这个进程继续执行的过程中,if语句中pid<0不满足,但是pid= =0是true。所以输出i am the child process...

为什么看上去程序中互斥的两个分支都被执行了?在一个程序的一次执行中,这当然是不可能的;但是你看到的两行输出是来自两个进程,这两个进程来自同一个程序的两次执行。

fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,但只有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。

可以这样想象,2个进程一直同时运行,而且步调一致,在fork之后,他们分别作不同的工作,也就是分岔了。这也是fork为什么叫fork的原因。 

在程序段里用了fork()之后程序出了分岔,派生出了两个进程。具体哪个先运行就看该系统的调度算法了。

如果需要父子进程协同,可以通过原语的办法解决。


高手二:

进程的创建:
创建一个进程的系统调用很简单.我们只要调用fork函数就可以了.
#include <unistd.h>

pid_t fork();


当一个进程调用了fork以后,系统会创建一个子进程.这个子进程和父进程不同的地方只有他的进程ID和父进程ID,其他的都是一样.就象父进程克隆(clone)自己一样.当然创建两个一模一样的进程是没有意义的.为了区分父进程和子进程,我们必须跟踪fork的返回值. 当fork掉用失败的时候(内存不足或者是用户的最大进程数已到)fork返回-1,否则fork的返回值有重要的作用.对于父进程fork返回子进程的 ID,而对于fork子进程返回0.我们就是根据这个返回值来区分父子进程的. 父进程为什么要创建子进程呢?前面我们已经说过了Linux是一个多用户操作系统,在同一时间会有许多的用户在争夺系统的资源.有时进程为了早一点完成任务就创建子进程来争夺资源. 一旦子进程被创建,父子进程一起从fork处继续执行,相互竞争系统的资源.有时候我们希望子进程继续执行,而父进程阻塞,直到子进程完成任务.这个时候我们可以调用wait或者waitpid系统调用.

   总结一下有三:

1,派生子进程的进程,即父进程,其pid不变;

2,对子进程来说,fork返回给它0,但它的pid绝对不会是0;之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;

3,fork之后父子进程除非采用了同步手段,否则不能确定谁先运行,也不能确定谁先结束。认为子进程结束后父进程才从fork返回的,这是不对的,fork不是这样的,vfork才这样。

Linux下的C编程入门之进程控制与进程通信编程

  • 0

    开心

    开心

  • 0

    板砖

    板砖

  • 0

    感动

    感动

  • 0

    有用

    有用

  • 0

    疑问

    疑问

  • 0

    难过

    难过

  • 0

    无聊

    无聊

  • 0

    震惊

    震惊

编辑推荐
Linux下C编程,进程通信之标准流管道通信(即系统调用) 1.概述   在编程的过程中总会需要使用系
信号本质 信号是在软件层次上对硬件的中断机制的一种模拟,在原理上,一个进程接受到一个信号和处理
在文章《linux基础编程:进程通信之信号》中,我们看到信号作为进程之间的通信方式。但是传送的信息
进程间通信概述 需要进程通信的原因: 数据传输 资源共享 通知事件 进程控制 Linux进程间通信(IPC
这篇文章介绍的内容包括 1.Linux进程--进程标识号 2.进程控制--进程创建 1.Linux进程--进程标识号
linux管道 管道相关内容的简介 管道是单向的字节流,它将某个进程的标准输出连接到另一个进程的标准
共享内存 最为高效的进程间通信方式 进程直接读写内存,不需要任何数据的拷贝   •为了在多个进程
一、进程基础 进程:进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就创建了一个
Linux的多任务编程-进程 进程的概念 进程是指一个具有独立功能的程序在某个数据集合上的一次动态执
Linux下的进程通信基本上是从Unix平台上的进程通信手段继承而来的。而对Unix发展做出重大贡献的两大
版权所有 IT知识库 CopyRight © 2009-2015 IT知识库 IT610.com , All Rights Reserved. 京ICP备09083238号