linux系统程序设计教程


linux系统程序设计教程

第一章:生成一个Process(进程)

进程是什么?简单地说,进程就是在执行状态下的一个程序(包括CPU状态,所占内存的状态,等等)
A进程生成了B进程,也就是说,A程序在执行的时候,又生成了另一个进程B。这个时候,我们可以把A进程叫做父进程,把B进程叫做子进程。
例程序:

// Usage : ./a.out 20
#include
int main( int argc , char *argv[])
{
int dep_time;
dep_time = atoi( argv[1] )*60 ; //将参数中给出的20(分钟)转换成整型的秒数
if( fork()==0 ) //生成子进程,然后父进程中止
{
sleep( dep_time );
fprintf( stderr , !!!!!!!! );
}
return 0;
}

上面的程序是一个闹钟程序。当你执行之后。程序不会显示什么,而是一下就回到UNIX的提示符下。但是你在命令行中指定了20分钟后你有事,那么在你执行这个程序之后20分钟,他会提醒你到时间了。
本程序只是做示例用,没有检查参数是否正确,等等。
生成一个新的进程,可以使用 fork() 函数 。以下说说fork()函数。

头文件: #include
形式 pid_t fork();
参数 无
返回值 成功时: 父进程中:子进程的PID (Process ID)
子进程中:0
失败时: 父进程中:-1
由于失败,没有生成子进程;

fork()刚执行完的时候,子进程和父进程是完全一模一样的两份进程(当然,PID是不一样的)。他们的各个变量的值都是一样的,而且都认为自己已经执行完fork()了。fork()后,区分父进程和子进程,只要看fork()的返回值就行了。
if( fork()==0 ) printf(这是子进程);
else printf(这是父进程);

同理:
if( fork()==0 )
{
//接下来要子进程做的工作
}
else
{
//接下来要父进程做的工作
}

一般,我们会把fork()返回给父进程的值保存下来(其实就是子进程的PID),等到需要结束子进程的时候,我们关掉他,如下:

pid_t child_pid ;
child_pid=fork();
if( child_pid==0 )
{
// ... ...
}
else
{
// ... ...
}
// ... ...需要结束子进程的时候
kill( child_pid , SIGKILL ) // kill()函数是用来发给另一个进程一个消息的。以后再讲。

先写这些,试试手。喜欢就顶。要是没人爱看我就不写了。呵呵。省得大家说我乱贴垃圾。
以后计划贴的东西:
在程序中执行UNIX命令或者另一个程序
取得环境变量并利用
UNIX文件系统(在程序中取得分区信息,等等)
使用管道操作达到在各进程互相交流数据
信号(signal)
进程间共享内存
用message实现进程间共享信息



第二章:在程序中执行UNIX命令或者其它程序

在UNIX下,像DOS的command.com那样的程序,我们称之为外壳(shell)。外壳就是一个命令解释器,你在外壳的提示符下输入命令(如同DOS的提示符一样),系统便会执行。
DOS的提示符一般是C:>,当然,你想改成什么样就能改成什么样,又当然,像BBS一样贴张图上去是不太现实的。
UNIX的提示符根据外壳的不同是不同的。
为了更好地说明本章想讲解的内容,我们先做一个外壳试试(玩具级别的)。我们给他起名叫SSH(Sohu Shell)吧。想取名叫CSH,可惜CSH在没生我之前就有了。呵呵。

 1 /* 简单的外壳程序 */
 2 #include
 3 int main()
 4 {
 5 static char prompt[64]=> ;
 6 char command[256];
 7 int st;
 8 
 9 fprintf(stderr,%s,prompt); // 在屏幕上输出提示符
10 while(gets(command)!=NULL) // 取得命令
11 {
12 if(fork()==0) // 生成子进程
13 { // 这里是子进程接下来要做的事
14 if( execl(command,command,(char *)0)==(-1) )
15 // 上一句是执行命令 
16 exit(1); // 当出错时子进程异常中止
17 }
18 else
19 { // 父进程
20 wait(&st); // 等待子进程结束
21 fprintf(stderr,%s,prompt);
22 // 输出提示符,等待命令
23 }
24 }
25 return 0;
26 }
View Code

执行方法:
%./ssh
>/bin/ls
当前目录下文件名一览
>Ctrl+D
%

普通的外壳在执行exit命令后会关闭。也就是说,退出一层外壳。咱们这个程序现在还做不到。愿意的话加上这个功能试试好了。所以要关闭这个外壳就得来点狠的。Ctrl+D,Ctrl+C什么的。再不你就再开一个外壳然后ps -ef再kill。再狠一些……拆硬盘,拨电源

我们这里有了一个新的函数:execl()。其实他是一组函数中的一个。这组函数如下:
int execl( path , arg0 , arg1 , ... , argn , (char *)0 );
int execv( path , argv );
int execle( path , arg0 , arg1 , ... , argn , (char *)0 , envp );
int execve( path , argv , envp );
int execlp( file , arg0 , arg1 , ... , argn , (char *)0 );
int execvp( file , argv );
其中的参数定义如下:
char *path;
char *file;
char *arg0 , *arg1 , ... , *argn;
char *argv[];
char *envp[];
返回值: 成功时:所执行的命令将会覆盖现有的进程,所以无返回值
失败时:-1
用过TC的朋友应该知道,TC的Process.h里有一个system()函数。这组函数其实和system()的功能差不多。

比方说:
execl( /bin/ls , /bin/ls , -al , /home , (char *)0 );
或者
char *argv[];
strcpy( argv[0] , /bin/ls );
strcpy( argv[1] , -al );
strcop( argv[2] , /home );
execv( /bin/ls , argv );
都相当于在命令行下敲入了/bin/ls -al /home并且回车。(引号不是命令里的。是我解释时加上去的。别看混了)。
execle()和execve(),函数名最后一个字母都是e。就是说,这两个函数在调用其它程序的同时,还可以把环境变量一起传给被调程序
execlp()和execvp(),函数名最后一个字母都是p,就是说,这两个函数在使用的时候,就算你不指定命令文件所在的路径,它也会根据环境变量PATH去挨个地方找。找着就执行。找不着拉倒。
比方说:
setenv $path = ( /bin $path ) 这句话将环境变量PATH的第一个路径设为/bin。这是在SHELL下执行的。C里没这东西吧。
在程序中这样用这个函数
execlp( ls , ls , -al , /home , (char *)0 );
与上面的效果一样。当然。如果你PATH变量没设好的话。它就不一定找到哪儿去了。

还有一个函数是wait(),说明如下:
#include
pid_t wait(int *stat_loc);
返回值就是一个PID了。忘了PID是什么意思的朋友光顾一下我上一篇贴子。
它的参数有些意思。其实它与你的子进程用什么方式结束有关系。当你的子进程以exit()方式结束的话,stat_loc所指向的地址的前8位将会是exit()的参数的后8位,而stat_loc所指向的地址的后8位是0。比方说:你的子进程是exit(1);那stat_loc所指向的地址的内容应该是0000 0001 0000 0000。

exit():
#include
void exit(int status);
就算你在程序中没写exit()函数,编译器也是认为你是在最后加入了这个函数。



第三章:增强ssh的功能(使用环境变量)

还记得上次做的那个ssh吧?这回咱们把它再改一改。
大家知道。C语言的main函数是这样的:
int main( int argc , char *argv[] , char *envp );
前两个不用说了吧。其实知道前两个的话应该也知道第三个:取得系统环境变量。
UNIX和DOS一样。有着各种各样的环境变量(明确地说,应该是比DOS用得更广泛)。比方说常用的$PATH,$HOME,$USER等等。如果用的是csh,可以改 ~/.cshrc或者干脆直接在命令行下设就行了。这些是UNIX的东西,不属于我们在C/C++讨论的东西了。有兴趣的朋友可以到UNIX版去看一看。
下面是一个取得系统环境变量并输出的小程序


/* getenv.c 取得系统环境变量并输出 */
#include
int main ( int argc , char *argv[] , char *envp[] )
{
int i;
for( i=0 ; envp[i]!=NULL ; i++ )
{
printf( %s , envp[i] );
}
return 0;
}
编译执行后应该是这样:
%./getenv
DISPLAY=:0.0
HOME=/home/syuui
USER=syuui
以及你的系统中其它的环境变量。
想一想它是怎么运行的:当你在命令行下敲入getenv并回车,shell就fork出一个新的进程,然后让这个进程去执行getenv,并把现在系统中的各个环境变量存入到envp里去。这个时候,原来shell的进程就是getenv进程的父进程。envp的参数是从父进程中取得的。现在你知道上一节中为什么有那两个带p的函数了?
上一回做的ssh这个外壳的命令是带不了参数的。因为咱们的程序不知道要去读参数。这回不妨做一个能读参数的试试

 1 #include 
 2 #define SP 0
 3 #define NOSP 1
 4 void getarg( char *argv[] , char *p ); //取得各个参数
 5 int main()
 6 {
 7 static char prompt[64]=> ;
 8 char command[256], *argv[256], *p;
 9 int st;
10 
11 fprintf( stderr , %s , prompt );
12 while( (p=gets(command))!=NULL )
13 {
14 getarg( argv , p );
15 if( fork()==0 )
16 {
17 if( execv(argv[0],argv)==(-1) )
18 exit(1);
19 }
20 else
21 {
22 wait( &st );
23 fprintf( stderr , %s , prompt);
24 }
25 }
26 return 0;
27 }
28 void getarg( char *argv[] , char *p )
29 {
30 int i , sp_flag ;
31 sp_flag=SP; //SP代表空格,NOSP代表非空格的意思
32 for( i=0 ; *p!='' ; p++ )
33 {
34 if( sp_flag==SP && *p!=' ' )
35 //如果现在状态是读过空格,但现在这个字母是非空格
36 //那很显然,读到一个新的参数
37 {
38 argv[i]=p;
39 i++;
40 sp_flag=NOSP;
41 }
42 if( *p==' ' )
43 {
44 *p='';
45 sp_flag=SP;
46 }
47 }
48 argv[i]=(char *)0;
49 }
View Code

这篇文章东西说得比较少。给大家出个问题吧:
看到了吧。C能做的事情很多。谁说C不支持多进程?那是因为DOS。呵呵
上回做的ssh,必须输入绝对路径才能执行。现在要求,咱们做一个只输入命令以及参数,程序自动在$PATH里定义的各路径查找这个命令。找到就执行,找不到就报错的外壳出来试试?有做出来的,请贴程序。

2003-7-27 关于进程状态的一些补足:
众所周知,UNIX是一个多用户多任务的操作系统。所谓多任务,就是指在同一个时间内,看上去有许多任务在同时执行。这一点是与DOS不同的。所以Turbo C下找不到fork()函数。当然。大家知道,实际上CPU同一个时间只能处理一件事。它是在轮流执行这些进程,由于速度很快。所以看起来像是所有进程在一起跑。同样,一个进程,它有使用CPU的时候,也有等待使用CPU的时候。这就决定了进程的几种状态。
进程大致可以分为“执行中”,“准备执行”,“睡眠中”三种状态。
执行中:进程在占用CPU。
准备执行:想要使用CPU,但是CPU被别的进程占用中,所以等待CPU空闲。
睡眠中:等待事件发生的进程。比方说,等待输入输出结束的进程。
我们用fork()生成一个新的进程,或者用exec函数组覆盖当前进程后。当这个子进程结束的时候要给父进程送去一个信号(signal,后述),然后转为zombie状态。zombie状态是指一个进程已经结束,所占内存空间等等已经返还给系统,但是在系统的进程列表中仍然存在的这么一个状态。当父进程执行wait()后,子进程才真正完全结束。如果父进程先一步结束了,那么由init代替父进程。所以,wait()不只是等待子进程结束。它还有上面所说的这个任务。
我们写个程序来看一下:

/* zombie */
#include
int main()
{
int st;
if( fork()==0 )
{
exit(1); //子进程生成后直接结束
}
else
{
sleep( 300 ); //休眠300秒
wait( &st );
printf(Return code=%d ,i);
}
return 0;
}

编译后执行
%./zombie &
这个时候,程序在后台运行
%ps -ef | grep zombie
看一下,是不是有一个进程处在zombie状态?

第四章:文件系统

UNIX所管理的机器一般是大型机而不是PC。所管理的硬盘一般也非常大。所以一般分成几个区,每个区都有其单独的文件系统。比方说你能大概能找到这样的一些文件
/dev/sd/c0t0d0s0
/dev/sd/c0t0d0s1
... ...
当UNIX启动的时候,分区被挂装(mount)并统一成一个树状的文件系统
分区的物理构造咱们暂且放在一边,先写个程序,读一下分区信息试试。

/* ndf.c 计算参数所指定的分区的剩余空间比率 */
#include
#include
#include

int main(int argc , char *argv[])
{
struct statvfs buf[1];
sync();
if( statvfs(argv[1],buf)!=0 )
{
fprintf(stderr , Cannot read super block ! );
exit(1);
}
fprintf(stderr , %4.1f %% free ,
(float)buf[0].f_bfree / buf[0].f_blocks*100 );
return 0;
}

编译执行:
%./ndf /
49.8 % free

这里用了一个statvfs函数。其具体如下:
#include
#include
int statvfs( char *path , struct statvfs *buf );
返回值: 成功时: 0
失败时: -1
还有一个sync()函数。用来更新分区的super block;
void sync();
UNIX系统为了加快处理速度,将分区的super block信息读到内存中保存。sync()函数就是把在内存中保存的super block信息再写回到硬盘上去。

UNIX系统使用好几种文件系统。有S5,ufs,VxFS等等。虽然这些文件系统的构造非常不同,但一通百通,咱们在这几篇贴子里只讨论一下比较容易理解,而且“经典的”S5文件系统。(别的我也不会。呵呵)
S5: 文件名最长14字节,构造简单
ufs: 文件名最长255字节,BSD所用的文件系统。
VxFS: Veritas Softwave公司开发的文件系统。出现错误时可以快速恢复。由于这种文件系统保证文件在硬盘上连续存放,所以处理速度很快。

现在说一下S5分区的构造
一个分区包含如下四部分(按顺序):
[ boot block ]
[ super block ]
[ i node block ]
[ data block ]

boot block :
这个部分在分区的最开始处,用来存放引导程序。就算是不能引导的分区一样有boot block,这个时候这部分就没有用了。不过一般这部分也不大。大多数只有512或者1024字节。
super block :
super block在boot block之后,用来存放这个分区全体的管理信息。上面那个ndf.c就是读的这部分所存储的信息。里边存放了i node block的大小,free block数组等等。根据这些信息可以得知data block的开始位置。
i node block :
i node是index node的缩写。i node block就是存放i node的部分
UNIX把一切都看成是个文件。包括目录以及设备等等的所有的文件都有一个i node号,作为这个文件的管理信息。文件本身存在于数据区,但是i node号存在i node block里。主要包含文件的模式,链接数,文件所有者,文件大小,在硬盘上的位置,最后读写时间,最后更新时间等信息。
为了加快存储速度,系统会把一定数量的i node存至内存。UNIX系统不一样,存多少也就不一样。
data block :
这部分就是存放数据本身的了。这部分被分成一定大小的块,如同DOS的扇区一样。一般大小是1024字节,分到4096的也有。

解说到这里,我们再来写个程序。打开一个目录,然后把这个目录下所有的文件的i node号及文件名输出来。

/* nls.c */
#include
#define DIRSIZ 14
int main( int argc , char *argv[] )
{
struct dir
{
int i_no;
char f_name;
};
struct dir dir_data[1];
FILE *fp;
fp=fopen(argv[1],r);
while( fscanf(fp,%i%s,&(dir_data[0].i_no),dir_data[0].f_name)!=EOF )
{
printf(%i %s , dir_data[0].i_no , dir_data[0].f_name );
}
fclose(fp);
return 0;
}

%./nls /
... ...
2048 usr
2049 home
... ...

别忘了,在UNIX下,目录也当成文件。
最近,为了使目录的格式变得通用而不再依赖于操作系统,程序中大多使用统一的格式。这种情况下,我们最好就不直接用fopen()打开目录,而使用opendir(),readdir()等函数比较好。重写一下上面的程序。

 1 /* nls2.c */
 2 #include 
 3 #include 
 4 int main( int argc , char *argv[] )
 5 {
 6 DIR *fp;
 7 struct dirent *p;
 8 fp=opendir(argv[1]);
 9 while( (p=readdir(fp))!=NULL )
10 {
11 printf(%i %s
, p->d_ino , p->d_name );
12 }
13 closedir(fp);
14 return 0;
15 }
View Code

执行结果和上面一样。函数说明如下:
#include
DIR *opendir( char *dirname ); 打开一个目录,成功时返回DIR结构体的地址,失败返回NULL
struct dirent *readdir( DIR *dirp ); 取得打开目录中下一个文件的信息,成功时返回下一个文件信息的地址,失败时,或者读至末尾时返回NULL
int closeidr( DIR *dirp ); 关闭目录,成功时返回0,失败时返回-1

注意:readdir在成功地读出一项之后,会自动指向下一项。假设你有如下程序:
struct dirent *p;
p=readdir(fp);
p++; //千万不要像这行这样写。你无法保证这目录里的文件信息是连续存放的。

你只要一遍一遍地用readir()这个函数就行了。它帮你全搞定



第五章:目录及文件操作

上一章我们说了一些UNIX的文件系统的物理构造。下面我们来看看具体怎么对文件进行操作。当然这里所说的文件及目录操作不是fopen()。呵呵。我们要做一些fopen办不到的事。

 1 /* newer.c 比较参数所指定的两个文件
 2 将其中新的文件的文件名输出来 */
 3 #include 
 4 #include 
 5 #include 
 6 int main( int argc , char *argvp] )
 7 {
 8 struct stat buf[2] , *p ;
 9 if( argc!=3 )
10 {
11 fprintf( stderr , Usage : %s file1 file2
 , argv[0] );
12 exit(1);
13 }
14 p=buf;
15 if( stat(argv[1],p)!=0 ) //取得第一个文件的信息
16 {
17 fprintf( stderr , %s not found !
 , argv[1] );
18 exit(1);
19 }
20 p++;
21 if( stat(argv[2],p)!=0 ) //取得第二个文件的信息
22 {
23 fprintf( stderr , %s not found !
 , argv[2] );
24 exit(1);
25 }
View Code

if( buf[0].st_mtime > buf[1].st_mtime ) //比较最终更新时间
printf( %s , argv[1] );
else
printf( %s , argv[2] );
return 0;
}

执行结果:
%newer afile bfile
bfile

使用stat()函数,可以得到文件的信息。这些信息是在i node中保存的这个文件信息的一部分。得到的信息将会保存在stat.h中定义的stat型的结构体中。stat()函数解释如下:
#include
#include
int stat( char *path , struct stat *buf );
返回值: 成功时:0
失败时:-1

我们再来写一个玩玩

 1 #include 
 2 #include 
 3 #include 
 4 #define MASK 0555 //这个数字的意思等一下解释,他代表“可读”和“可执行”
 5 int main( int argc , char *argv[] )
 6 {
 7 struct stat buf[1];
 8 mode_t mode;
 9 if( argc!=2 )
10 {
11 fprintf( stderr , Usage : %s file
 , argv[0] );
12 exit(1);
13 }
14 if( stat(argv[1],buf)!=0 )
15 {
16 fprintf( stderr , Cannot read i-node
 );
17 exit(1);
18 }
19 mode = ( buf[0].st_mode & MASK ); //计算文件新的权限标志
20 if ( chmod(argv[1],mode)!=0 ) //改变文件的权限标志
21 {
22 fprintf( stderr , Cannot change mode
 );
23 exit(1);
24 }
25 return 0;
26 }
View Code

现在来解释一下0555这个数字的意思。
众所周知,UNIX是一个多用户多任务的操作系统。每个用户有自己的权限,这个权限限制了用户可以做哪些事,不可以做哪些事。对于文件来说,用户可以分成四类:
root(根用户,超级用户)
这个用户是系统的管理呐,权限最大,可以随意读写任何文件。
owner(文件拥有者)
实际拥有文件的用户。文件属主。
group(组成员)
用户所在的用户组的成员
other
以上三类之外的其它用户

UNIX中,每个文件信息中包括一组9位的权限标志。分别给文件属主,用户组和其他用户指定对该文件的读、写和执行权。请看下面的例子:
%ls -l /bin/ls
-rwxr-xr-x 1 root bin 27281 Aug 15 2002 /bin/ls*
重要是看-rwxr-xr-x,第一个 - 表示这是一个普通文件,这个位置也可以出现些别的字符,比方说目录的话这里会是 d 。而 l 表示一个链接。余下的9位可以分成三段,每段三位。本例中从左至右
rwx 表示文件的属主拥有文件的读,写,执行权
r-x 表示同组的用户拥有文件的读,执行权(注意 ,“写”权限的位置是一个 - )
r-x 表示其它的用户拥有文件的读,执行权
文件的访问权限还可以用三位8进制来表示。如上例
rwx r-x r-x 可以换成
111 101 101 (有该权限,则该位为1,否则为0)
换成8进制, 二进制的111是八进制的7 , 二进制的101是八进制的5。
现在看看0555是什么意思?就是说,可以读,可以写的意思。
把0555和原来文件的权限标志做与运算,得到的新的权限标志就是把原来的文件权限标志中所有的写权限全取消了。其余权限变。然后在程序中用chmod()把这个新的权限标志赋给文件即可。

chomd()函数用法如下:
#include
#include
int chmod( char *path , mode_t mode );
返回值: 成功时:0
失败时:-1

关于目录,还有另一个比较有用的函数,即chdir()。用这个函数可以在程序中转换当前目录。
#include
#include
int chdir( char *path );
返回值: 成功时:0
失败时:-1


以上两章,简单地叙述了一下UNIX的文件系统以及在UNIX C中对文件的操作方法。并列举了常用的一些函数。



第六章:标准输入输出以及管道操作

标准输入输出大概所有的操作系统都差不多吧。从键盘输入。从屏幕输出。除非你用的还是打纸带的老家伙。呵呵。
主要说一下管道操作。注意:此处所说的管道操作不是
% cat -n test.c | more
这是在提示符状态下使用管道,把第一个程序(cat)的输出作为输入传给第二个程序(more)。我们现在要在程序中使用的管道原理与此相同。将某一个程序的输出变为另一个程序的输入。

做一个石头剪子布的程序。其中包括一个父程序和一个子程序。注意是两个程序,不是两个函数。也不是两个进程。不过因为父程序运行的时候要通过exec()函数来执行子程序。所以我们也可以把它看成是两个进程。
父进程取得用户的输入(石头S,剪子C,布P中的某一个,P=0,S=1,C=2)并通过管道传给子进程
子进程取得父进程传来的数字,加上自己的PID后做为种子数,生成一个随机数然后被3除,得出来一个余数是0、1或者2。再通过管道传回给父进程。
父进程根据两个数字(用户输入的,以及子进程传回来的)判定胜负,输出

 1 /* parent. */
 2 #include 
 3 #include 
 4 int main()
 5 {
 6 int i , j ,
 7 st , fd[2] ;
 8 pid_t pid ;
 9 static int result[3][3]={1,0,2,2,1,0,0,1,2};
10 char argv1[3] , argv2[3] ,
11 ch ;
12 
13 ch=getchar();
14 switch(ch)
15 {
16 case 'P': i=0;break;
17 case 'S': i=1;break;
18 case 'C': i=2;break;
19 default:
20 fprintf( stderr , Enter P , S or C , Please!
 );
21 exit(1);
22 }
23 if( pipe(fd)!=0 ) //建立管道
24 {
25 fprintf( stderr , PIPE Error !
 );
26 exit(1);
27 }
28 sprintf( argv1 , %d , fd[0] );
29 sprintf( argv1 , %d , fd[1] );
30 
31 switch( pid=fork() ) // fork出一个新的进程,执行子程序
32 {
33 case 0:
34 if( execl(child,child,argv1,argv2,(char *)0) == (-1)) exit(1);
35 //执行了子程序
36 break;
37 case -1:
38 fprintf( stderr , fork Error !
 );
39 exit(1)
40 }
41 write( fd[1], &i , sizeof(i) ); //向管道写数据
42 wait( &st ); //等待子程序结束
43 read( fd[0] , &j , sizeof(j) ); //从管道读数据
44 switch(result[i][j])
45 {
46 case 0:
47 printf( You won
 );
48 break;
49 case 1:
50 printf( Same
 );
51 break;
52 case 2:
53 printf( You lost
 );
54 }
55 close(fd[0]);
56 close(fd[1]);
57 return 0;
58 }
View Code
 1 /* child.c */
 2 #include 
 3 int main(int argc , char *argv[])
 4 {
 5 int i,j,
 6 read_fd,
 7 write_fd;
 8 read_fd=atoi( argv[1] ); //设定输入用管道
 9 write_fd=atoi( argv[2] ); //设定输出用管道
10 read( read_fd , &i , sizeof(i) ); //从管道中取得数据
11 srand( i+getpid() ); //设定随机数的种子数
12 j=rand()%3; //生成随机数
13 write( write_fd , &j , sizeof(j) ); //写向管道
14 close( read_fd );
15 close( write_fd );
16 return 0;
17 }
View Code

编译执行父程序即可。

这种管道是用来在父子进程间传递数据的。如同大家在程序中所见:父进程开辟管道,然后生成子进程执行子程序,并将管道参数作为main()函数的参数传给子程序。通过一个相同的管道实现读写。开辟管道时,我们用到了这个函数:

int pipe( int fd[2] ); 开辟一个管道

参数fd[2]是一个有两个元素的数组。可以看成是两个管道的记述子。fd[0]用来读,fd[1]用来写。
返回值: 成功时:0
失败时:-1
读取/写入管道时,我们用到了下面函数

读取管道中的数据
int read( int fd , char *buf , unsigned int nbyte );
向管道中写入数据
int write( int fd , char *buf , unsigned int nbyte );

其中,fd是管道记述子,也就是我们前面说的fd[0]或者fd[1],buf装数据,nbyte指定读/写数据的数量,单位是字节。成功时返回0,失败时返回-1。

由于准备考研。这篇文章耽误了一些时日。最近还有些事,也许下一篇也得几天后才能再贴。
另外,在此向诸位致歉。我的程序是在学校的UNIX下写的,一般是用软盘带回来,写上一篇贴子程序的时候没有带软盘,只好打印出来回来再敲。在输入的时候有一个错误(现已改正)
int main( int argc , char *argvp]);
应为
int main( int argc , char *argv[]);



第七章:UNIX进程(Process)及信号(signal)

在进程执行过程中,如果出现什么事件(event),系统将会给进程一个信号。进程可以在得到这个信号后做些适当的处理。
还是与以前一样,咱们先来个小程序吧。

 1 /* slot.c */
 2 #include 
 3 #include
 4 
 5 int x[3];
 6 long kin;
 7 
 8 int kekka();
 9 int main( int argc , char *argv[] )
10 {
11 srand( getpid() ); /* 设定随机数的种子数 */
12 signal( SIGKILL , kekka ); /* 当Ctrl+C按下时,执行kekka函数 */
13 kin = atoi( argv[1] ); /* 取得赌注数目 */
14 
15 printf(press Ctrl+C to stop maching !
);
16 while(1)
17 {
18 x[0]=rand()%10;
19 x[1]=rand()%10;
20 x[2]=rand()%10;
21 printf(%d %d %d
,x[0],x[1],x[2]);
22 fflush( stdout );
23 }
24 return 0;
25 }
26 int kekka() 
27 {
28 if( x[0]==x[1] && x[1]==x[2] ) 
29 printf(Great , you won %d 
 , kin*3000);
30 else
31 printf(You lost %d
,kin);
32 exit(0); /* 游戏结束。退出程序 */
33 }
View Code

执行方法:
%./slot 100

执行后。画面上会出现一排排的数字。当你按下Ctrl+C的时候,程序得到信号,跳转至kekka()函数入,判断随机数。然后退出程序。
这里有这样一个新函数。是我们在UNIX程序设计中时常用得到的。

#include
int ( *signal( int sig , void (*func()) ))();
返回值: 成功时:进程中断地址
错误时:SIG_ERR

这个函数挺不好懂呵。解释解释。它的意思是,当收到SIG这个信号时,跳转至func()这个函数处继续执行。
int sig这个参数其实好办。下面我们会给出一个表。将其所有的参数一并列出。这是在头文件中#define的,代表各种不同的信号。
void ( * func() )
这个参数是一个函数指针。意思是在SIG信号被传给进程后,所要执行的部分。
此外,func()这个参数还可以为如下值:
SIG_DEL 系统的默认动作
SIG_IGN 忽略这个信号。

signal作用只有一次,当一个信号发生,触发signal动作的之后,signal就不起作用了。比方说,在上例中,如果你把所有的exit(0)全去掉(不退出程序),程序会进入无限循环中。你按一次Ctrl+C,signal执行一次,显示出结果。然后继续循环,生成随机数,输出。你再按Ctrl+C,signal就不好使了。
如果需要重复使用signal()函数,可以考虑在signal调用的动作函数中再加一个signal,比方说在上例的程序中再加一个 signal( SIGKILL , kekka );

加了signal这个函数后,整个程序如果没收到信号,则正常执行。否则跳转至signal中指定的func()函数处继续。
当然,我们不可能,也没有必要去为每一个信号指定一个动作。当未指定动作的信号发生的时候,系统会执行默认的动作。比方说我们知道:当Ctrl+C按下的时候,正常默认的动作是结束程序。但是,Ctrl+C按下时,发生的信号是SIGKILL,如果你为SIGKILL这个信号指定了一个动作的话,系统将去执行你指定的这个动作,而不管原来默认的了。
打个比方让大家更容易懂一些:
比方说:你在睡午觉。突然来了一个美女(帅哥)(Ctrl+C)让你陪她/他去逛街购物共进晚餐……(此处删去若干字),你正常的默认的动作是马上起来陪她出去玩(程序中止)。但是你妈说不许去,你妈说你得在家擦窗户,你只好留在家擦窗户(signal()指定的动作)。擦完窗户之后你可以选择陪美女去逛街(默认动作,Ctrl+C的埸合是退出程序,即上例程序中的exit(0)),可以选择继续睡觉(回到原来中断的地方继续执行,上例程序中如果没有exit(0),便会回到中断处继续执行)。
(大哥。打个比方嘛。干嘛拿臭鸡蛋砸我……)

signal种类非常多,在此不一一列出。

使用这个函数,你可以防止用户按下Ctrl+C结束程序。还可以做很多其它的事——只有你想不到的,没有C做不到的。

注意:根据UNIX系统的不同,signal的定义是不一样的。比方说,有的老式UNIX工作站上SIGINT是按下del键后发生的信号,而有些机型上刚是按下Ctrl+Z发生的。在使用时要注意。



第八章:向其它进程传递信号

上一章,我们简单地说了一下信号。信号不仅仅可以在本进程内使用,还可以向其它的进程传递。确切地说,一般情况下,一个进程收到的信号绝大部分是从系统的INIT传来的。而INIT也是一个进程。
用过UNIX或者LINUX的朋友大概都知道,UNIX系的操作系统里,有一个kill命令。如果你的一个进程无法中止,可以使用kill命令强行干掉它。
%kill mypro
我们自己也可以做个简单的kill命令。

 1 /* nkill.c */
 2 #include 
 3 #include 
 4 #include
 5 
 6 int main( int argc , char *argv[] )
 7 {
 8 pid_t pid;
 9 if( argc != 2 ) // 检查命令行参数个数
10 {
11 fprintf( stderr , Usage : %s PID 
 , argv[0] );
12 exit(1);
13 }
14 pid = atol( argv[1] );
15 if( kill( pid , SIGKILL )!=0 ) // 向pid这个进程传送一个SIGKILL信号
16 {
17 fprintf( stderr , kill failed
 );
18 }
19 return 0;
20 }
View Code

执行例:
%sleep 300 & (后台睡眠300秒)
[1] 520
%nkill 520
[1] + Killed sleep 300

你用自己写的程序杀死了一个sleep进程。没事。杀了就杀了吧。留着也是垃圾……
说明一下,众所周知,UNIX是非常注意用户的权限的。所以,如果你向你自己生成的进程发送SIGKILL的话,这个进程会中止。如果你是向别的(你没有写权限的)进程发送SIGKILL的话,除非那个进程允许你动它,否则你发了也是白发。打个不恰当的比较下流一些的比方:如果你要求亲一下你自己老婆的话(你有“亲”的权限),你会如愿以偿。但是如果你想亲一下别人的老婆的话(你没有“亲”的权限),除非他及她同意,否则……呵呵。你死定了。
所以,执行上面的程序的时候,你必须保证你有权限关掉那个进程。可不是说你ps -ef然后随便挑一个进程就能关的。要不那黑客也太好当了。

几乎是咱们的老规矩了:先写个程序,然后解释这个程序,说说其中新出现的函数。这回还照旧吧。
大家看到了。这里新出来一个kill()函数。解释如下:

#include
#include

int kill( pid_t pid , int sig );
返回值: 成功时:0
失败时:-1
这个函数是向进程ID(PID)为pid的进程传送一个sig信号。
这个函数有什么用呢?用处可大了。
在上一章中,我们用到了SIGKILL信号。这一节我们用的还是这个信号。实际上,信号(signal)有许多。而且根据UNIX系统的不同,这些信号也不太一样。

我们简单列举一些信号例子。其用途请恕小生才疏,解释不清。有兴趣的朋友可以自己查阅一下资料。
SIGHUP , SIGINT , SIGQUIT , SIGILL , SIGTRAP , SIGABRT , SIGEMT , SEGFPE(注意,这里是SEG),SIGKILL , SIGBUS , SIGSEGV , SIGSYS , SIGPIPE , SIGALRM , SIGTERM , SIGUSR1 , SIGUSR2 , SIGCLD , SIGPWR , SIGWINCH , SIGSTOP , SIGTSTP , SIGCONT , SIGTTIN , SIGTTOU
使用kill()函数,可以将这些信号中的一个或者多个送至另一个进程。刚才的程序中,我们就是将一个SIGKILL信号送至sleep那个进程,该进程收到信号,以为是用户按下了Ctrl+C,就中止了。

知道了这些, 我们再来做一个比较有意思的程序。我们做一个cat程序(类似于DOS下的type,查看文本文件的内容)。不过这个cat程序与UNIX的cat不太一样,我们把由参数指定的文件的内容输出到屏幕上。如果你没指定文件,那么这个程序会等你五秒,在这五秒钟里,你可以从标准输入输入一个文件。然后显示。万一这五秒钟里你也没输入什么文件名。程序会把当前目录下的所有文件名打印出来(ls),以督促你输入一个文件名。

 1 /* show.c */
 2 #include 
 3 #include 
 4 #include
 5 
 6 jmp_buf env;
 7 
 8 int func();
 9 int main( int argc , char *argv[] )
10 {
11 char filename[256];
12 signal( SIGALRM , func ); // 如果收到一个SIGALRM信号,则转至func处执行
13 if( argc==1 )
14 {
15 fprintf( stderr , & );
16 alarm(5); // 5秒钟后向自己发送一个SIGALRM信号
17 setjpm(env); // 设置记号
18 scanf( %s , filename );
19 execl( /usr/bin/more , more , filename , (char *)0 );
20 }
21 else
22 {
23 execl( /usr/bin/more , more , argv[1] , (char *)0 );
24 }
25 return 0;
26 }
27 int func()
28 {
29 int st;
30 fprintf( stderr , Which file do you want to read ?

 );
31 if( fork()==0 )
32 {
33 execl( /bin/ls , ls , -FCa , (char *)0 );
34 }
35 wait(&st);
36 longjmp( env , 0 ); // 跳转至先前的setjmp()处
37 }
View Code


这个程序看明白了吗?可能有些不太好懂。我们从头跑一遍试试。
首先是装入头文件等等。没关系了。然后signal( SIGALRM , func ),即,当收到一个SIGALRM信号的时候,中断现在所做的工作。转而执行func()函数。
不过可惜得很,现在暂时还没人给咱们发送SIGALRM信号,我们继续往下看。下面是判断命令行参数了。如果只有这个程序的可执行文件一个参数的话(也就是说,没有指明要显示哪个文件的内容),那就打印出一个&号来。然后设置alarm(5),也就是说,在5秒后给自己发一个SIGALRM信号。下一句setjpm(env)是设置一个记号,以后当执行至longjmp()函数的时候,就会跳转到这里来。然后等待你输入文件名。
如果在这五秒种里,你输入了一个文件名的话,那么就向下执行——通过execl()函数调用系统的more命令,显示该文件的内容。然后退出。这只需要要很少的一段时间。绝对用不上一秒(看你当前目录下有多少文件了。没人会在一个目录下装上1G吧?)。最后结束程序。alarm()在5秒后发出的信号乐意咋地咋地去吧。咱不用管了。
问题是:如果在这五秒钟里,你没有输入文件名的话,事情就变得非常微妙了。这种情况下,程序将停留在
scanf( %s , filename );
这一行。等待你输入文件名。然后五秒种的期限到了。alarm()函数向这个进程发了一个SIGALRM信号。signal()函数检测到这个信号,转向func()函数处执行。
func()函数先是输出了一个提示信息 Which file do you want to read ? 。然后fork()出一个子进程。这个子进程通过execl()函数调用/bin/ls命令,显示出当前目录下的所有文件。execl()函数生成的进程覆盖掉了子进程。显示完所有文件名后退出。父进程等待子进程结束(wait())后,执行longjmp()函数,返回到刚才的setjmp那行程序结尾处。继续执行下一步,也就是再执行一次 scanf( %s , filename )。当然,上一次的scanf()已经中止了。剩下的就简单地一路到底。明白了?
为什么要fork()出一个子进程,而不用原来的进程呢?是因为execl()函数会覆盖掉当前进程。如果直接使用execl()的话,咱们这个宝贝进程就被execl()覆盖,调用完 /bin/ls 后直接结束退出了。还怎么跳来跳去的呀。呵呵。

好。又多了三个函数:

unsigned alarm( unsigned sec );
在sec指定的秒数之后,向本进程发送一个SIGALRM信号。
返回值是以前的alarm()函数执行后所剩余的时间。

还记不记得第一章中,咱们做过的闹钟程序了?那个时候是让进程休眠一段时间,然后再报时。现在我们有了另一个法宝——alarm()。这回也别让进程浪费大好时光睡大觉去了。让它做点别的什么事吧。写写试试?

#include
int setjmp( jmp_buf env ); // 设置跳转记号env
void longjmp( jmp_buf env , int val ); // 跳转至记号env处。

jmp_buf env就是跳转记号了。那么longjmp第二个参数 int val 是干什么用的呢?当你用longjmp()函数跳转至setjmp()处的时候,setjmp()函数应该会返回一个值吧?这个值就是int val,也就是longjmp()第二个参数。
setjmp()的返回值,在第一次使用的时候是0。在由longjmp()跳转过来的时候,返回值是longjmp()的第二个参数。
但是有一个特例:像刚才程序中所写的,longjmp( env , 0 ),longjmp()第二个参数是 0 的时候,setjmp()的返回值是 1 。

有很多界面比较友好的程序中,当等待用户输入数据的时候,如果用户过了一段时间还没有输入的话,程序会输入一些提示信息,或者是可供选择的选项来供用户参考。在Windows编程中,例如VC,VB等等,大概可以用定时器 timer 或者其它的什么办法来解决(我粗通VB。VC一点都不会)。在UNIX下,上面那样便是一种解决方法。当然。编程习惯不同,解决方法也不尽相同。

信号(signal)这个东西,在UNIX编程中占很重要的地位。笔者一直想用通俗的语言来解释,可是限于笔者的语文水平,所以做到的仅此而已。总的来说,signal可以看成是一种软中断。当一个signal被发送到程序的时候,会引发程序的默认过程(比方说SIGKILL的场合是强制结束)或者程序的作者自定义的过程(用signal()函数指定)。
另外。longjmp()和setjmp()两个函数由于使用了跳转(类似于goto语句,但是longjmp()能实现函数外跳转,就是说能跳到另一个函数里去。goto办不到吧),这不符合结构化程序设计的要求,所以如果不是万不得以,不推荐使用。



第九章:利用共享内存实现进程间通信

在两个进程间共享数据的方法,至今为止我们只说过利用管道。管道只是利用了输入输出重定向的原理,非常简单。而且只能在父子进程间使用。很多场合下这种方法是无法满足我们的要求的。
那么现在,我们又有了一种新的方法——利用共享内存(shared memory)。这可以使我们在任意两个进程间传递数据,而且也是相对简单容易实现的一个方法。
注意:在正常情况下,一个进程的所使用的内存区是不允许其它进程访问的。这里我们要开辟的共享内存是例外。
我们来做一个简单的剪贴板。从标准输入向mcut输入数据,mcut将其放在共享内存中,然后mpaste从共享内存中读出数据并显示。

 1 /* mcut.c */
 2 #include 
 3 #include 
 4 #include 
 5 #include
 6 
 7 int main()
 8 {
 9 key_t shmkey;
10 int shmid , in_tmp ;
11 char *head , *pos ,
12 in_data[4096] , *in_ptr ;
13 
14 shmkey = ftok( mcut , 'a' ); // 计算标识符
15 // shmid开辟共享内存
16 shmid = shmget( shmkey , sizeof(in_data) , IPC_CREAT | 0666 ) ;
17 
18 head = pos = shmat( shmid , 0 , 0 ); // 允许本进程使用这块共享内存
19 
20 in_ptr = in_data ;
21 // 开始从标准输入输入数据,暂时存在in_data里。
22 while( (in_tmp=getchar()) != EOF )
23 {
24 *in_ptr = in_tmp ;
25 in_ptr++ ;
26 }
27 
28 *in_ptr = '' ;
29 in_ptr = in_data ;
30 
31 //
View Code

 开始写入共享内存

while( *in_ptr != '' )
{
*pos = *in_ptr ;
pos++;
in_ptr++;
}
*pos = '' ;
shmdt( head ); // 禁止本进程使用这块内存
return 0;
}

 1 /* mpaste.c */
 2 #include 
 3 #include 
 4 #include 
 5 #include
 6 
 7 int main()
 8 {
 9 key_t shmkey;
10 int shmid;
11 char *head , *pos ,
12 out_data[4096] , *out_ptr ;
13 
14 shmkey = ftok( mcut , 'a' ); // 计算标识符
15 // 开辟共享内存
16 shmid = shmget( shmkey , sizeof(out_data) , IPC_ALLOC | 0666 );
17 head = pos = shmat( shmid , 0 , 0 ); // 允许本进程使用这块共享内存
18 out_ptr = out_data ;
19 
20 // 从共享内存中取得数据
21 while( *pos != '' )
22 {
23 *out_ptr = *pos ;
24 out_ptr++ ;
25 pos++ ;
26 }
27 *out_ptr = '' ;
28 printf( %s
 , out_data );
29 fflush( stdout );
30 shmdt( head ); // 禁止本进程使用这块共享内存
31 return 0;
32 }
View Code

如何?明白多少了?
要使用共享内存,应该有如下步骤:
1.开辟一块共享内存 shmget()
2.允许本进程使用共某块共享内存 shmat()
3.写入/读出
4.禁止本进程使用这块共享内存 shmdt()
5.删除这块共享内存 shmctl()或者命令行下ipcrm

上面的程序中用到了如下函数,我们一一解释一下。

首先是ftok()。它有两个参数,一个是字符串,一个是字符。字符串一般用当前进程的程序名,字符一般用来标记这个标识符所标识的共享内存是这个进程所开辟的第几个共享内存。ftok()会返回一个key_t型的值,也就是计算出来的标识符的值。

其次,操作共享内存,我们用到了下面的函数
#include
#include
#include

int shmget( key_t shmkey , int shmsiz , int flag );
void *shmat( int shmid , char *shmaddr , int shmflag );
int shmdt( char *shmaddr );

shmget()是用来开辟/指向一块共享内存的函数。参数定义如下:
key_t shmkey 是这块共享内存的标识符。如果是父子关系的进程间通信的话,这个标识符用IPC_PRIVATE来代替。但是刚才我们的两个进程没有任何关系,所以就用ftok()算出来一个标识符使用了。
int shmsiz 是这块内存的大小.
int flag 是这块内存的模式(mode)以及权限标识(关于权限的意思,请参阅本系列第五章)。
模式可取如下值: 新建:IPC_CREAT
使用已开辟的内存:IPC_ALLOC
如果标识符以存在,则返回错误值:IPC_EXCL
然后将“模式” 和“权限标识”进行“或”运算,做为第三个参数。
如: IPC_CREAT | IPC_EXCL | 0666
这个函数成功时返回共享内存的ID,失败时返回-1。


shmat()是用来允许本进程访问一块共享内存的函数。
int shmid是那块共享内存的ID。
char *shmaddr是共享内存的起始地址
int shmflag是本进程对该内存的操作模式。如果是SHM_RDONLY的话,就是只读模式。其它的是读写模式
成功时,这个函数返回共享内存的起始地址。失败时返回-1。

shmdt()与shmat()相反,是用来禁止本进程访问一块共享内存的函数。
参数char *shmaddr是那块共享内存的起始地址。
成功时返回0。失败时返回-1。

此外,还有一个用来控制共享内存的shmctl()函数如下:
#include
#include
#include

int shmctl( int shmid , int cmd , struct shmid_ds *buf );
int shmid是共享内存的ID。
int cmd是控制命令,可取值如下:
IPC_STAT 得到共享内存的状态
IPC_SET 改变共享内存的状态
IPC_RMID 删除共享内存
struct shmid_ds *buf是一个结构体指针。IPC_STAT的时候,取得的状态放在这个结构体中。如果要改变共享内存的状态,用这个结构体指定。
返回值: 成功:0
失败:-1

刚才我们的mpaste.c程序中还可以加入这样几句。

struct shmid_ds buf;
... ...
shmctl( shmid , IPC_STAT , &buf ); // 取得共享内存的状态
... ...
shmctl( shmid , IPC_RMID , &buf ); // 删除共享内存

注意!!!!!!!!!:在使用共享内存,结束程序退出后。如果你没在程序中用shmctl()删除共享内存的话,一定要在命令行下用ipcrm命令删除这块共享内存。你要是不管的话,它就一直在那儿放着了。
简单解释一下ipcs命令和ipcrm命令。

取得ipc信息:
ipcs [-m|-q|-s]
-m 输出有关共享内存(shared memory)的信息
-q 输出有关信息队列(message queue)的信息
-s 输出有关“遮断器”(semaphore)的信息
%ipcs -m

删除ipc
ipcrm -m|-q|-s shm_id
%ipcrm -m 105



第十章:利用消息队列实现进程间通信

所谓消息功能,是指由一个进程产生并送出的消息,被放在一个叫做“消息队列”的列中管理,然后由接收方从消息队列中取出消息,这么一种功能。(先入先出)

做一个例子程序看看:
下面的两个程序分为发送方和接收方。发送方将自己的PID和从标准输入输入的一个字符串送至消息队列,接收方把消息队列中所有的PID和字符串读出,并输出至标准输出。

 1 /* msend.c */
 2 #include 
 3 #include 
 4 #include 
 5 #include
 6 
 7 int main()
 8 {
 9 int msqid;
10 key_t msgkey;
11 struct msgbuf
12 {
13 long mtype;
14 char mdata[256];
15 };
16 struct msgbuf msgdata , *p ;
17 
18 p = &msgdata ;
19 printf(Enter message : );
20 fflush( stdin ); /* 刷新标准输入缓冲区 */
21 
22 gets( p->mdata ); /* 输入字符串 */
23 p->mtype = getpid();
24 msgkey = ftok ( mrecv , 'a' ); /* 计算标识符 */
25 msqid = msgget( msgkey , IPC_CREAT | 0666 ) ; /* 建立消息队列 */
26 msgsnd( msqid , p , sizeof(p->mdata) , 0 ); /* 送消息 */
27 return 0;
28 }
View Code
 1 /* mrecv.c */
 2 #include 
 3 #include 
 4 #include 
 5 #include
 6 
 7 int main()
 8 { 
 9 int msqid;
10 key_t msgkey;
11 struct msgbuf
12 {
13 long mtype;
14 char mdata[256];
15 };
16 struct msgbuf msgdata , *p ;
17 p = &msgdata ;
18 msgkey = ftok( mrecv , 'a' ); /* 计算标识符 */
19 msqid = msgget( msgkey , IPC_CREAT | 0666 ) ; /* 取得消息队列的ID */
20 while(1)
21 {
22 msgrcv( msqid , p , sizeof(p->mdata) , 0 , 0 ) ; /* 读消息 */
23 printf(Message received from %ld
%s
 , p->mtype , p->mdata );
24 }
25 return 0;
26 }
View Code

执行例:
% msend
Enter message : Hello , world
%msend
Enter message : I am Syuu I

%mrecv
Message received from 321
Hello , world
Message received from 326
I am Syuu I

使用消息功能,可分如下几步
发送方:
1.生成消息队列,取得ID。
2.向消息队列送消息。
接收方:
1.根据标识符,取得ID。
2.从消息队列接收消息。
3.删除消息队列。

例程序相对来说比较简单。我们就不解释了。

消息队列的使用方法以及函数的构造等等基本上和上一章所讲的共享内存的使用方法一样。可参考上一章来理解本章内容。
说说出现的函数:
#include
int msgget( key_t msgkey , int flag );
取得一个消息队列的ID,如不存在则建立。
返回值: 成功时:消息队列的ID
失败时:-1

int msgsnd( int msqid , struct msgbuf *msgp , size_t msgsiz , int msgflag );
向消息队列送消息
返回值: 成功时:0
失败时:-1
msqid是消息队列的ID,size_t msgsiz是结构体成员mdata的大小,msgflag与上一章所讲的共享内存的flag起一样的作用,不过,当这个参数为IPC_NOWAIT的时候,如果消息队列已满,则返回错误值。如果不为IPC_NOWAIT,在消息队列已满 的情况下,会一直等到消息队列有空地方的时候再发送。
注意这里的这个 struct msgbuf *msgp 。要求的格式如下:
struct msgbuf
{
long mtype;
char mdata[256];
};
long mtype在这里我们用来保存本进程的PID。mdata则是保存要发送的数据。由于mdata的大小不一定(根据实际需要定义),所以这个结构体并没有事先定义好。但是我们定义这个结构体的时候一定要遵循这个规定。你可以改的,只有mdata的大小,和结构体的名称。尽量不要修改结构体成员的名称和类型。实际上,根据mtype,我们还可以有所选择地接受消息。这在下面将会谈到。

int msgrcv( int msqid , struct msgbuf *msgp , size_t msgsiz , long msgtyp , int msgflag );
从消息队列取得一个消息
返回值: 成功时:0
失败时:-1
msqid , *msgp , msgsiz不用说了。long msgtyp是结构体msgbuf的mtype成员。msgflag与上述一样。只不过为IPC_NOWAIT的时候,如果消息队列是空的,则等到有消息可读的时候再读。当不为IPC_NOWAIT的时候,如果消息队列是空的,则返回错误值(与字面上理解的有些相反)

同样地,为了控制管理消息队列,一样有一个函数msgctl()如下:
#include
#include
#include

int msgctl( int msqid , int cmd , struct msqid_ds *buf );
返回值: 成功时:0
失败时:-1
cmd所指定的值与共享内存部分相同。

本来还应该有一章关于Semaphore的。其使用方法于共享内存和消息队列差不多,只不过使用场合不同。可是我自己也没太弄明白那个东东是怎么回事,为了避免以讹传讹……就不写了。

看了我这么多贴子,大家辛苦了。《UNIX系统程序设计》这一系列的贴子到此宣告完成。在这些文章的创作过程中得到了SOHU CC++论坛诸位朋友的大力支持。在此笔者深表谢意。
接下来,如果有时间的话,我会继续写一套《UNIX网络程序设计(C语言版)》。不知大家有兴趣否。
再有一点。本文中所解释的所有的函数,严格地说,应该叫:系统调用( system call )。



写在后面的话:

UNIX是一个“古老的”,高级的,复杂的,稳定的,有着广泛用户的,难以掌握的,功能强大的,不容易学会的,变态的,操作系统。与Microsoft的windows系列相比,具有对网络更强的操作能力,对进程更强的管理能力,对数据更高的保护能力。然而不幸地,由于windows系列更便利的操作能力对新手的诱惑,国内使用UNIX的人日渐式微。
然而,如果你将来在IT业发展的话,UNIX是必不可少的一门技能。试看当今网络的世界,绝大多数的服务器上仍然是运行着UNIX系的操作系统。UNIX以其强大的功能,广泛的高级用户为基础,在这个MS泛滥的时代依然屹立不倒。
UNIX虽然是一个商业软件,但其发展经历了悠久的年代(在计算机史上绝对可以这样说)。世界各国都有从事UNIX开发的人员。况且还有Linux作为其新生力量,其核心技术的公开性/安全性是以美元为目标的MS所开发的windows所不及的。目前我国有识之士已发出呼吁,建议政府机关/军队等关键部门的计算机采用Linux操作系统,以避免出现紧急情况(战争等)时受制于人。
众所周知,程序开发要分平台,除非你用的是JAVA之类的与平台关系不大的语言。目前在我国,堪称当前技术的生力军、未来技术的主力军的年轻一代计算机专业学生/从业人员/有志者,绝大部分学习/从事着windows平台的程序设计。
诚然,windows平台上也有许多优秀的开发工具,如VC++ , VB , Delphi等等。然而UNIX程序设计的重要性日趋显著。掌握一些UNIX平台上程序设计的基本知识,对于对于程序设计的认识,乃至将来立足于IT业界,都有着重要的影响。

原文地址:https://www.cnblogs.com/sun-frederick/p/4763391.html