linux C进程相关总结
进程与线程
创建进程
- fork()函数
fork()函数
进程状态
- ps -aux:查看当前进程状态
main()
进程创建后通常要调用 exec 族函数来装载程序文件的可执行映像,并在完成装载后调
用程序的 main()函数。在 C 程序中, main()函数通常是程序的执行起始点.
int main(); /* 原型 1 */
int main(int argc, char *argv[]); /* 原型 2 */
int main(int argc, char *argv[], char *env[]); /* 原型 3 */
argc 表明命令行参数的个数;
argv 是指向参数的各个指针所构成的数组
argv[0]为程序的名称,后续的数组元素组成参数列表
argv[argc]值为 NULL;
原型 3 的 env参数是指向环境变量字符串的数组
进程ID
- 获取当前进程的进程ID
#include<unistd.h>
pid_t getpid(void)
ps -ef:shell命令输出各个进程的ID
pid_t:整形数据和int差不多
#include<unistd.h>
#include<sys/types.h>
uid_t getuid(void);//获取进程的实际用户标识符
uid_t geteuid(void);//获取调用进程的有效用户标识符
pid_t getgid(void);//获取调用进程的有效组标识符
pid_t getegid(void);//获取调用进程的实际组标识符
#include<unistd.h>
#include<sys/types.h>
#include<stdio.h>
int main()
{
printf("getpid:%d\n",getpid()); //进程标识符
printf("getpid:%d\n",getpid()); //父进程标识符
printf("getuid:%d\n",getuid()); //获取进程的实际用户标识符
printf("getegid:%d\n",getegid()); //获取调用进程的实际组标识符
printf("geteuid:%d\n",geteuid()); //获取调用进程的有效用户标识符
printf("getgid:%d\n",getgid()); //获取调用进程的有效组标识符
printf("getegid:%d\n",getegid()); //获取调用进程的实际组标识符
}
父进程与子进程
进程创建时,创建进程为新进程的父进程,新进程是创建进程的子进程。
子进程获取父进程的PID:
#include<unistd.h>
pid_t getppdid(void)
/*打印进程之父的进程*/
printf("parent pid = %d\n,getppid()");
pstree:以树形的方式展示进程关系的输出
UID和GID
UID:用户ID
GID:用户组ID
id root:列出root的UID和GID
环境变量
3种方式获取运行环境的环境变量
- (1) mian的第三个参数env
#include <stdio.h>
int main(int argc,char *argv[],char *env[]){
int i = 0;
while(env[i])
{
puts(env[i++]);
}
return 0;
}
- (2) 通过 environ 全局变量获取
#include <stdio.h>
extern char ** environ;
int main(int argc, char * argv[]) {
int i = 0;
while (environ[i])
puts(environ[i++]);
return 0;
}
- (3) 通过 getenv()函数获取。
#include <stdlib.h>
char *getenv(const char *name);//name为要获取的环境变量名,返回值为该变量的值
标准IO
- 标准输入、标准输出和标准错误。这 3 个文件的描述符分别是 0、 1、 2。在 unistd.h 头文件中用如下宏来表示这 3 个文件描述符:
#include <unistd.h>
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
- 在 glibc 中用 3 个 FILE 类型的指针来表示这 3 个文件,分别是: stdin、 stdout、 stderr,定义如下:
#include <stdio.h>
extern FILE *stdin;
进程基本操作
fork创建进程
#include <unistd.h>
pdi_t fork(void)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
pid_t pid;
pid = fork(); /* 创建进程 */
if (pid == 0) { /* 对子进程返回 0 */
printf("Here is child, my pid = %d, parent's pid = %d\n", getpid(), getppid()); /* 打印父子进程 PID */
exit(0);
} else if(pid > 0) { /*对父进程返回子进程 PID */
printf("Here is parent, my pid = %d, child's pid = %d\n", getpid(), pid);
} else { /* fork 出错 */
perror("fork error\n");
}
return 0;
}
进程创建后,子进程与父进程开始并发执行, 执行顺序由内核调度算法来决定。
fork()函数如果成功创建了进程, 就会对父子进程各返回一次,其中对父进程返回子进程的 PID,对子进程返回 0;失败则返回小于 0 的错误码.
- 执行结果
由图我们可以看出子进程打印出来的父进程和父进程打印的自己进程并不相同。什么原因呢?
你仔细看下者段代码,父进程执行完了就自动退出了
如果我在改成这样呢?
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
pid_t pid;
pid = fork(); /* 创建进程 */
if (pid == 0) { /* 对子进程返回 0 */
printf("Here is child, my pid = %d, parent's pid = %d\n", getpid(), getppid()); /* 打印父子进程 PID */
while(1);//新增
exit(0);
} else if(pid > 0) { /*对父进程返回子进程 PID */
printf("Here is parent, my pid = %d, child's pid = %d\n", getpid(), pid);
while(1);//新增
} else { /* fork 出错 */
perror("fork error\n");
}
return 0;
}
vfork创建进程
#include <sys/types.h>
#include <unistd.h>
pdi_t vfork(void)
fork和vfork的区别
- fork要复制父进程的数据段;vfork不需要完全复制父进程的数据段,在子进程没有调用exec系列函数或exit函数之前,子进程与父进程共享数据段。
- vfork函数会自动调用exec系列函数去执行另外一个程序
- fork不对父子进程的执行次序进行任何限制;而vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec系列函数或exit之后,父子进程的执行次序才不再有限制。
例如:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
pid_t pid;
int count = 0;
pid = vfork(); /* 创建进程 */
if (pid == 0) { /* 对子进程返回 0 */
printf("Here is child, my pid = %d, parent's pid = %d\n", getpid(), getppid()); /* 打印父子进程 PID */
while(1){
usleep(1000);
printf("%d\n",count++);
}
exit(0);
} else if(pid > 0) { /*对父进程返回子进程 PID */
printf("Here is parent, my pid = %d, child's pid = %d\n", getpid(), pid);
while(1){
usleep(1000);
printf(“father\n");
}
} else { /* fork 出错 */
perror("fork error\n");
}
return 0;
}
你会发现它一直执行子进程,不退出。
如果我们把这段代码中的vfork更改为fork,我们可以看出父子进程会自动轮询执行的,也可以看出时间片为多少。
终止进程
- 正常终止:
1.从main函数return返回
调用类exit()函数
- 常见异常终止:
调用abort函数
接收到一个信号终止
exec族函数
exec族函数用来修改当前进程的代码空间
下面一个范例讲述如何使用exec族函数为子进程装载新程序
- “/home/peng/”目录下有可执行程序 sample3
/*
example3
*/
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
extern char **environ; /* 全局环境变量 */
int main(int argc, char *argv[]) {
int i;
printf("argc = %d\n", argc); /* 打印参数个数 */
printf("args :");
for (i = 0 ; i < argc; i++)
printf(" %s ", argv[i]); /* 打印参数表 */
printf("\n");
i = 0;
while(environ[i])
puts(environ[i++]); /* 打印环境变量表 */
printf("\n");
return 0;
}
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
char *env_init[] = {"USER=peng", "HOME=/home/peng/", NULL}; /* 为子进程定义环境变量 */
int main(int argc, char *argv[]) {
pid_t pid;
if ((pid = fork())< 0) { /* 创建进程失败判断 */
perror("fork error");
} else if (pid == 0) { /* fork 对子进程返回 0 */
execle("/home/peng/sample3", "sample3", "hello", "world", (char *)0, env_init);/*子进程装载新程序*/
perror("execle error"); /* execle 失败时才执行 */
exit(-1);
} else {
exit(0); /* 父进程退出 */
}
return -1;
}
exit函数
一个进程执行完成之后必须要退出,退出时内核会进行一系列的操作,包含清洗缓冲区等,在linux中共有八中进程退出方法,其中包含五种正常退出和三种异常退出。通常linux的应用代码会调用exit系列的函数退出一个进程。
#include <stdlib.h>
#include <unistd.h>
void exit(int status);
void _exit(int status);
void _Exit(int status);
exit系列函数没有返回值,其使用一个终止状态(exit status)的整形变量作为参数,linux内核会对这个终止状态进行检查。当异常终止时,linux内核会直接产生一个终止状态字,描述异常终止的原因,可以通过wait或waitpid函数来获取终止状态字,父进程也可以通过检查终止状态来获取子进程的状态。
在以下三种状态下调用linux会认为该进程的终止状态是未定义,如果main函数返回值定义为整形并且main函数执行到最后一条语句返回,则该进程的终止状态是0.
注:在main函数中调return语句返回在绝大多数情况下是等效于调用exit系列函数的。
eixt函数与_exit函数最大的区别在于:
exit清空所有打开的 FILE*流的缓冲区并关闭流,然后删除所有由tmpfile()创建的临时文件。进程退出时,内核关闭所有剩下的已打开文件(即那些由open()、creat()或文件描述符继承打开的文件),释放其地址空间,然后释放所有其他使用的资源。exit()从不返回。
_exit函数直接使进程停止运行,清除内存空间,并销毁其在内核中的各种数据结构。
看个例子:`
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
pid_t pid;
pid = fork(); /* 创建进程 */
if (pid == 0) { /* 对子进程返回 0 */
printf("Here is child, my pid = %d, parent's pid = %d\n", getpid(), getppid()); /* 打印父子进程 PID */
printf("here is child");//没有\n,所以不会写数据
exit(0);//退出,强制清空,会输出上面未完成的数据
} else if(pid > 0) { /*对父进程返回子进程 PID */
printf("Here is parent, my pid = %d, child's pid = %d\n", getpid(), pid);
printf("here is parent");
_exit(0);
} else { /* fork 出错 */
perror("fork error\n");
}
return 0;
}
结果:
root@zcc:/home/zcc/test# ./a.out
Here is parent, my pid = 1919, child's pid = 1920
Here is child, my pid = 1920, parent's pid = 1919
here is child
wait()函数
wait()函数用来帮助父进程获取其子进程的退出状态。当进程退出时,内核为每一个进程保存了一定量的退出状态信息,父进程可根据此退出信息来判断子进程的运行状况。如果父进程未调用 wait()函数,则子进程的退出信息将一直保存在内存中。
由于进程终止的异步性,可能会出现子进程先终止或者父进程先终止的情况,从而出现两种特殊的进程:
-
僵尸进程:如果子进程先终止,但其父进程没有为它调用 wait()函数,那么该子进程就会变为僵尸进程。僵尸进程在它的父进程为它调用 wait()函数之前将一直占有系统的内存资源。
-
孤儿进程:如果父进程先终止,尚未终止的子进程将会变成孤儿进程。孤儿进程将直接被 init 进程收管,由 init 进程负责收集它们的退出状态。
调用 wait()函数的进程将挂起等待直到它的任一个子进程终止, wait()函数原型如下:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
参数 status 是一个用来保存子进程退出状态的指针, 若函数执行成功,将返回获取到退出状态进程的 PID,否则返回-1。
除 wait()函数外,还有更加灵活的 waitpid()函数可以完成收集子进程退出状态, waitpid可以指定专为特定子进程等待挂起。
- 获取子进程退出状态
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void print_exit_status(int status) { /* 自定义打印子进程退出状态函数 */
if (WIFEXITED(status)) /* 正常退出,打印退出返回值 */
printf("normal termination, exit status = %d\n", WEXITSTATUS(status));
else if (WIFSIGNALED(status)) /* 因信号异常退出,打印引起退出的信号 */
printf("abnormal termination, signal number = %d\n", WTERMSIG(status));
else
printf("other status\n"); /* 其它错误 */
}
int main(int argc, char *argv[]) {
pid_t pid;
int status;
if ((pid = fork()) < 0) { /* 创建子进程 */
perror("fork error");
exit(-1);
} else if (pid == 0) {
exit(7); /* 子进程调用 exit 函数,参数为 7 */
}
if (wait(&status) != pid) { /* 父进程等待子进程退出,并获取退出状态*/
perror("fork error");
exit(-1);
}
print_exit_status(status); /* 打印退出状态信息 */
if ((pid = fork()) < 0) { /* 创建第二个子进程 */
perror("fork error");
exit(-1);
} else if (pid == 0) {
abort(); /* 子进程调用 abort()函数异常退出 */
}
if (wait(&status) != pid) { /* 父进程等待子进程退出,并获取退出状态*/
perror("fork error");
exit(-1);
}
print_exit_status(status); /* 打印第二个退出状态信息 */
return 0;
}
守护进程
守护进程(Daemon) 是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,它不需要用户输入就能运行并提供某种服务。
守护进程的父进程是 init 进程,因为它真正的父进程在 fork 出该子进程后就先于该子进程 exit 退出了,所以它是一个由 init 领养的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出(无论是向标准输出设备还是标准错误输出设备的出) 都需要特殊处理。
- 常用创建守护进程的函数
#include <unistd.h>
int daemon(int nochdir, int noclose);
- nochdir: 0:将调用进程的工作目录设置为根目录,否则保持原有的工作目录不变。
- nocolse:0:会将标准输入/输出/错误重定向到/dev/null文件中,否则不改变这些文件描述符。
返回0-成功 -1:异常。
下例使用 daemon()函数创建守护进程的用法,变为守护进程后程序每60 秒打印当前的时间信息到/tmp/daemon.log 文件中。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <time.h>
int main(void)
{
int fd;
time_t curtime;
if(daemon(0,0) == -1) {
perror("daemon error");
exit(-1);
}
fd = open("/tmp/daemon.log", O_WRONLY | O_CREAT|O_APPEND, 0644);
if (fd < 0 ) {
perror("open error");
exit(-1);
}
while(1) {
curtime = time(0);
char *timestr = asctime(localtime(&curtime));
write(fd, timestr, strlen(timestr));
sleep(60);
}
close(fd);
return 0;
}
信号
信号名称 | 信号描述 |
---|---|
SIGALRM | 在用alarm()函数设置的计数器超时时,产生此信号 |
SIGINT | 当用户按中建(ctrl+c)时,终端产生此信号并发送给前台进程组的进程 |
SIGALRM | 在用alarm()函数设置的计数器超时时,产生此信号 |
SIGALRM | 在用alarm()函数设置的计数器超时时,产生此信号 |
SIGKILL | 这是两个不能捕捉、忽略的信号之一,它提供了一种可以杀死任一进程的方法 |
SIGSTOP | 这是两个不能捕捉、忽略的信号之一,用于停止一个进程 |
SIGPIPE | 如果在写管道时读进程已经终止,则产生该信号 |
SIGTERM | 这是由 kill 命令发送的默认信号 |
SIGCHLD | 在一个进程终止或者结束时,将 SIGCHLD 信号发送给它的父进程 |
SIGABRT | 调用 abort()函数时产生此信号,进程异常终止 |
SIGSEGV | 该信号指示进程进行了一次无效的内存引用 |
SIGUSR1,SIGUSR2 | 这是用户定义的两个信号,可以用于应用程序间通信 |
- kill:向指定进程发送指定信号
kill -l:列出系统支持的所有信号
kill -s:指明要给进程发送什么样的信号,不使用默认发送SIGTERM.
- 信号函数
- sigaction函数
Linux 系统为大部分信号定义了缺省处理方法,当信号的缺省处理方法不满足需求时,可通过 sigaction()函数进行改变。 sigaction()函数的原型如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
sigaction()函数成功返回 0,否则返回-1。
signum :指出需要改变处理方法的信号,如 SIGINT 信号, 但 SIGKILL 和 SIGSTOP这两个信号是不可捕捉的。
act 和 oldact 是一个 sigaction 结构体的指针, act 是要设置的对信号的新处理方式,而 oldact 则为原来对信号的处理方式
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
- sa_handler 是一个函数指针, 用来指定信号发生时调用的处理函数;
- sa_sigaction 则是另外一个信号处理函数,它有三个参数,可以获得关于信号的更详细的信息; 当 sa_flags 成员的值包含了SA_SIGINFO 标志时,系统将使用sa_sigaction 函数作为信号的处理函数,否则将使用 sa_handler;
- sa_mask 成员用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号正被处理时,它本身会被自动地放入进程的信号掩码,因此在信号处理函数执行期间, 这个信号都不会再度发生。可以使用 sigemptyset()、 sigaddset()、 sigdelset()
分别对这个信号集进行清空、增加和删除被屏蔽信号的操作; - sa_flags 成员用于指定信号处理的行为,它可以是以下值的“按位或” 组合:
SA_RESTART:使被信号打断的系统调用自动重新发起;
SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到SIGCHLD 信号;
SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程;
SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数的执行期间仍能发出这个信号;
SA_RESETHAND:信号被处理后重新设置处理方式到默认值;
SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。 - re_restorer 成员则是一个已经废弃的数据域,不要使用。
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
void ouch(int sig) { /* 信号处理函数 */
printf("\nOuch! - I got signal %d\n", sig);
}
int main(int argc, char *argv[]) {
struct sigaction act;
act.sa_handler = ouch; /* 设置信号处理函数 */
sigemptyset(&act.sa_mask); /* 清空屏蔽信号集 */
act.sa_flags = SA_RESETHAND; /* 设置信号处理之后恢复默认的处理方式 */
sigaction(SIGINT, &act, NULL); /* 设置 SIGINT 信号的处理方法 */
while (1) { /* 进入循环等待信号发生 */
printf("sleeping\n");
sleep(1);
}
return 0;
}
- 程序运行后第 1 次按 Ctrl+c 后程序打印 Ouch信息,第 2 次按 Ctrl+c 时进程退出。
- kill函数
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
成功:0 否则:-1
pid:进程的PID sig:需要发送的信号
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void print_exit_status(int status) { /* 打印子进程退出状态信息 */
if (WIFEXITED(status))
printf("normal termination, exit status = %d\n", WEXITSTATUS(status));
else if (WIFSIGNALED(status)) /* 是否为信号引起的退出 */
printf("abnormal termination, signal number = %d\n", WTERMSIG(status));
else
printf("other status\n");
}
int main(int argc, char *argv[]) {
pid_t pid;
int status;
if ((pid = fork()) < 0) {
perror("fork error");
exit(-1);
}
else if (pid == 0) { /* 子进程 */
while(1) {
printf("chlid sleeping\n");
sleep(1);
}
exit(0);
}
else {
sleep(2);
printf("parent send SIGINT to child\n");
kill(pid, SIGINT); /* 向子进程发送 SIGINIT 信号 */
if (wait(&status) != pid) { /* 获取子进程的退出状态 */
perror("wait error");
exit(-1);
}
print_exit_status(status);
}
return 0;
}
可看到父进程使用 kill()函数向子进程发送SIGINT 信号后,子进程停止打印“child sleeping”,父进程使用 wait()函数获取子进程的退出状态可以发现它是由信号 2(SIGINT)引起退出的。
进程间通信
通信方式包括:管道(匿名管道和命名管道)、信号、信号量、共享内存、消息队列和套接字等方式。
- 管道
匿名管道主要用于两个有父子关系的进程间通信。
命名管道主要用于没有父子关系的进程间通信。
匿名管道
- 原型
创建匿名管道:
#include<unistd.h>
int pipe(int pipefd[2]);
成功:0
异常:1
参数 pipefd 是一个文件描述符数组,对应着所打开管道的两端,其中 pipefd[0]为读端,pipefd[1]为写端,往写端写的数据会被内核缓存起来,直到读端将数据读完。
下例
用匿名管道方式实现了向子进程传递字符串的功能。由于管道传输是半双工的,数据只能在单个方向上流动,父进程往子进程传数据时,父进程的管道读端和子进程的管道写端都可以先关闭。
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[])
{
int pipefd[2];
pid_t cpid;
char buf;
if (argc != 2) {//命令行参数只接受一个参数
fprintf(stderr, "Usage: %s <string>\n", argv[0]);
exit(EXIT_FAILURE);
}
if (pipe(pipefd) == -1) { /* 创建匿名管道 */
perror("pipe");
exit(EXIT_FAILURE);
}
cpid = fork(); /* 创建子进程 */
if (cpid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0) { /* 子进程读管道读端 */
close(pipefd[1]); /* 关闭不需要的写端 */
while (read(pip。;efd[0], &buf, 1) > 0)
write(STDOUT_FILENO, &buf, 1);
write(STDOUT_FILENO, "\n", 1);
close(pipefd[0]);
_exit(EXIT_SUCCESS);
}
else { /* 父进程写 argv[1]到管道 */
close(pipefd[0]); /* 关闭不需要的读端 */
write(pipefd[1], argv[1], strlen(argv[1]));
close(pipefd[1]); /* 关闭文件发送 EOF,子进程停止读*/
wait(NULL); /* 等待子进程退出 */
exit(EXIT_SUCCESS);
}
}
命名管道
命名管道也被称为 FIFO 文件, 它突破了匿名管道无法在无关进程之间通信的限制,使得同一主机内的所有的进程都可以相互通信。
同时命名管道是一个特殊的文件类型, 它在文件系统中以文件名的形式存在, 在 stat结构中 st_mode 指明一个文件结点是不是命名管道。
函数原型:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
- mkfifo()创建一个真实存在于文件系统中的命名管道文件
- pathname指定了文件名
- mode 则指定了文件的读写权限
- 函数成功返回 0,否则返回-1 并设置 errno
errno | 描述 |
---|---|
EACCES | 路径所在的目录不允许执行权限 |
EEXIST | 路径已经存在 |
ENOENT | 目录部分不存在 |
ENOTDIR | 目录部分不一个目录 |
EROFS | 路径指向一个只读的文件系统 |
mkfifo()创建命名管道文件后,需要使用这个管道进行通信的进程要先打开该管道文件,然后通过 read、 write 函数像操作普通文件一样进行通信。
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>
#define BUFSIZE 1024 /* 一次最大写 1024 个字节 */
int main(int argc, char *argv[])
{
const char *fifoname = "/tmp/fifo"; /* 命名管道文件名 */
int pipefd, datafd;
int bytes, ret;
char buffer[BUFSIZE];
if (argc != 2) { /* 带文件名参数 */
fprintf(stderr, "Usage: %s < filename >\n", argv[0]);
exit(EXIT_FAILURE);
}
if(access(fifoname, F_OK) < 0) { /* 判断文件是否已存在 */
ret = mkfifo(fifoname, 0777); /* 创建管道文件 */
if(ret < 0) {
perror("mkfifo error");
exit(EXIT_FAILURE);
}
}
pipefd = open(fifoname, O_WRONLY); /* 打开管道文件 */
datafd = open(argv[1], O_RDONLY); /* 打开数据文件 */
if((pipefd > 0) && (datafd > 0)) { /* 将数据文件读出并写到管道文件 */
bytes = read(datafd, buffer, BUFSIZE);
while(bytes > 0) {
ret = write(pipefd, buffer, bytes);
if(ret < 0) {
perror("write error");
exit(EXIT_FAILURE);
}
bytes = read(datafd, buffer, BUFSIZE);
}
close(pipefd);
close(datafd);
}
else {
exit(EXIT_FAILURE);
}
return 0;
}
实现了通过命名管道发送文件的功能。程序先判断命名管道是否已存在,如果不存在则创建命名管道/tmp/fifo 文件,然后将参数所指出文件的数据循环读出并写进命名管道中。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>
#include <string.h>
#define BUFSIZE 1024
int main(int argc, char *argv[])
{
const char *fifoname = "/tmp/fifo"; /* 命名管道文件名,需对应写进程 */
int pipefd, datafd;
int bytes, ret;
char buffer[BUFSIZE];
if (argc != 2) { /* 带文件名参数 */
fprintf(stderr, "Usage: %s < filename >\n", argv[0]);
exit(EXIT_FAILURE);
}
pipefd = open(fifoname, O_RDONLY); /* 打开管道文件 */
datafd = open(argv[1], O_WRONLY|O_CREAT, 0644); /* 打开目标文件 */
if((pipefd > 0) && (datafd > 0)) { /* 将管道文件的数据读出并写入目标文件 */
bytes = read(pipefd, buffer, BUFSIZE);
while(bytes > 0) {
ret = write(datafd, buffer, bytes);
if(ret < 0) {
perror("write error");
exit(EXIT_FAILURE);
}
bytes = read(pipefd, buffer, BUFSIZE);
}
close(pipefd);
close(datafd);
} else {
exit(EXIT_FAILURE);
}
return 0;
}
实现了通过命名管道获取数据并保存成文件的功能。程序打开命名管道文件/tmp/fifo,然后循环从命名管道中读取数据并将它们写入由参数给出的文件中。
共享内存
共享内存是允许两个不相关的进程访问同一个逻辑内存的进程间通信方法, 是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。
POSIX 共享内存区涉及四个主要步骤:
-
指定一个名字参数调用 shm_open, 以创建一个新的共享内存区对象(或打开一个已经存在的共享内存区对象);
-
调用 mmap 把这个共享内存区映射到调用进程的地址空间;
-
调用 munmap() 取消共享内存映射;
-
调用 shm_unlink()函数删除共享内存段。
在编译 POSIX 共享内存应用程序时需要加上-lrt 参数。 -
打开或创建一个共享内存区
shm_open()函数用来打开或者创建一个共享内存区, 两个进程可以通过给 shm_open()函数传递相同的名字以达到操作同一共享内存的目的。
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
int shm_open(const char *name, int oflag, mode_t mode);
函数原型
函数成功返回创建或打开的共享内存描述符,与文件描述符作用相同,供后续操作使用,失败则返回-1
-
参数 name 为指定创建的共享内存的名称,其它进程可以根据这个名称来打开共享内存;
-
参数 oflag 为以下标志的或值:
-
- O_RDONLY:共享内存以只读方式打开
-
- O_RDWR:共享内存以可读写方式打开;
-
- O_CREAT:共享内存不存在才创建;
-
- O_EXCL:如果指定了 O_CREAT,但共享内存已经存在时返回错误;
-
- O_TRUNC:如果共享内存已经存在则将其大小设置为 0;
-
参数 mode 只有指定了 O_CREAT 后才有效, 用于设定共享内存的权限,与 open()函数类似。
-
删除共享内存
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
int shm_unlink(const char *name);
- 设置共享内存大小
#include <unistd.h>
#include <sys/types.h>
int ftruncate(int fd, off_t length);
函数成功返回 0,失败返回-1。
参数 fd 为需要调整的共享内存或者文件的描述符
length 为需要调整的大小。
- 映射共享内存
创建共享内存后,需要将这块内存区域映射到调用进程的地址空间中,可通过 mmap()函数来完成。
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
函数成功返回映射后指向共享内存的虚拟地址,失败返回 MAP_FAILED 值。
参数如下:
addr:指向映射存储区的起始地址, 通常将其设置为 NULL, 表示让系统来选择该映射区的起始地址;
- length:映射的字节数;
- prot:对映射存储区的保护要求,对指定映射存储区的保护要求不能超过文件 open模式访问权限。
- 它可以为以下选项的或值:
-
- PROT_READ: 映射区可读;
-
- PROT_WRITE:映射区可写;
-
- PROT_EXEC:映射区可执行;
-
- PROT_NONE: 映射区不可访问。
-
- flag:映射标志位,可为以下标志的或值:
-
- MAP_FIXED: 返回值必须等于 addr。 因为这不利于可移植性,所以不鼓励使用此标志;
-
- MAP_SHARED: 多个进程对同一个文件的映射是共享的,一个进程对映射的内存做了修改,另一个进程也会看到这种变化;
-
- MAP_PRIVATE: 多个进程对同一个文件的映射不是共享的,一个进程对映射的内存做了修改,另一个进程并不会看到这种变化。
- fd:要被映射的文件描述符或者共享内存描述符;
- offset: 要映射字节在文件中的起始偏移量。
- 取消共享内存映射
#include <sys/mman.h>
int munmap(void *addr, size_t length);
函数成功返回 0,否则返回-1;
参数 addr 为 mmap()函数返回的地址,
length 是映射的字节数。
取消映射后再对映射地址 调用收到SIGSEGV 信号。
注意:以下程序编译时需要增加-lrt库:详细描述
但gcc按描述中的编译方式没有通过,还是报错。后来通过如下方式编译。
例如:
实例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#define SHMSIZE 10 /* 共享内存大小, 10 字节 */
#define SHMNAME "shmtest" /* 共享内存名称 */
int main()
{
int fd;
char *ptr;
/* 创建共享内存 */
fd = shm_open(SHMNAME, O_CREAT | O_TRUNC | O_RDWR, S_IRUSR | S_IWUSR);
if (fd<0) {
perror("shm_open error");
exit(-1);
}
/* 设置共享内存大小*/
ftruncate(fd, SHMSIZE); /* 设置大小为 SHMSIZE */
ptr = mmap(NULL, SHMSIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);/*映射共享内存*/
if (ptr == MAP_FAILED) {
perror("mmap error");
exit(-1);
}
*ptr = 18; /* 往起始地址写入 18 */
munmap(ptr, SHMSIZE); /* 取消映射 */
shm_unlink(SHMNAME); /* 删除共享内存 */
return 0;
}
先创建共享内存,设置大小并完成映射,随后往共享内存起始地址写入一个值为 18 的整形数据,最后取消和删除共享内存。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#define SHMSIZE 10 /* 共享内存大小, 10 字节*/
#define SHMNAME "shmtest" /* 共享内存名称 */
int main()
{
int fd;
char *ptr;
fd = shm_open(SHMNAME, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); /*创建共享内存 */
if (fd < 0) {
perror("shm_open error");
exit(-1);
}
ptr = mmap(NULL, SHMSIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);/*映射共享内存*/
if (ptr == MAP_FAILED) {
perror("mmap error");
exit(-1);
}
ftruncate(fd, SHMSIZE); /* 设置共享内存大小 */
while (*ptr != 18) { /* 读起始地址,判断值是否为 18 */
sleep(1); /* 不是 18,继续读取 */
}
printf("ptr : %d\n", *ptr); /* 数据是 18,打印显示 */
munmap(ptr, SHMSIZE); /* 取消内存映射 */
shm_unlink(SHMNAME); /* 删除共享内存 */
return 0;
}
首先创建共享内存, 然后设置大小并映射共享内存, 最后检测共享内存首字节数据是否为 18,如果不是,继续等待,否则打印显示,并取消和删除共享内存。
共享内存的读进程先在后台运行,它循环等待写进程对共享内存做修改。当写进程完成修改后, 读进程将检测到共享内存单元的值发生了变化,然后打印出来并退出。
信号量
多进程编程中需要关注进程间的同步及互斥问题。同步是指多个进程为了完成同一个任务相互协作运行,而互斥是指不同的进程为了争夺有限的系统资源(硬件或软件资源)而相互竞争运行。
信号量是用来解决进程间同步与互斥问题的一种进程间通信机制,它是一个特殊的变量,变量的值代表着关联资源的可用数量。 若等于 0 则意味着目前没有可用的资源。
根据信号量的值可以将信号量分为二值信号量和计数信号量:
-
二值信号量: 信号量只有 0 和 1 两种值。 若资源被锁住,信号量值为 0,若资源可用则信号量值为 1;
-
计数信号量: 信号量可在 0 到一个大于 1 的数(最大 32767) 之间取值。该计数表示可用资源的个数。
-
信号量只能进行两个原子操作: P 操作: V 操作。
P 原子操作和 V 原子操作的具体定义如下。
- P 操作:如果有可用的资源(信号量值>0),则占用一个资源(将信号量值减 1);如果没有可用的资源(信号量值=0),则进程被阻塞直到系统将资源分配给该进程(进入信号量的等待队列,等到资源后再唤醒该进程)。
- V 操作:如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程;如果没有进程等待它,则释放一个资源(给信号量值加 1)
POSIX 提供两类信号量: 有名信号量和基于内存的信号量(也称无名信号量) 。
有名信号量可以让不同的进程通过信号量的名字获取到信号量,
而基于内存的信号量只能放置在进程间共享内存区域中。
有名信号量与基于内存的信号量的初始化和销毁方式不同,
编译 POSIX 信号量程序需要加上-pthread 参数。
创建或打开有名信号量
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);
信号量的类型为 sem_t, 该结构里记录着当前共享资源的数目。 sem_open()函数成功返回指向信号量的指针,失败返回 SEM_FAILED。
-
参数 name 为信号量的名字,两个不同的进程可以通过传递相同的名字打开同一个信号量
-
oflag 可以是以下标志的或值:
-
- O_CREAT:如果name指定的信号量不存在则创建,此时必须给出mode和value值;
-
- O_EXCL:如果 name 指定的信号量存在,而 oflag 指定为 O_CREAT | O_EXCL,则 sem_open()函数
-
mode 为信号量的权限位,类似于 open()函数
-
value 为信号量的初始化值。
关闭有名信号量
#include <semaphore.h>
int sem_close(sem_t *sem);
函数成功返回 0,失败返回-1,参数 sem 为需要关闭的信号量的指针。
初始化基于内存信号量
使用基于内存的信号量之前需要先用 sem_init()函数完成初始化,它的原型如下:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 函数成功返回 0,失败返回-1;
- 参数 sem 为需要初始化的信号量的指针;
- pshared 值如果为 0 表示该信号量只能在线程内部使用,否则为进程间使用。在进程间使用时,该信号量需要放在共享内存处;
- value 为信号量的初始化值,代表的资源数。
P操作
信号量的 P 操作由 sem_wait()函数来完成,它的函数原型如下:
#include <semaphore.h>
int sem_wait(sem_t *sem);
如果信号量的值大于 0, sem_wait()函数将信号量值减 1 并立即返回,代表着获取到资源,如果信号量值等于 0 则调用进程(线程)将进入睡眠状态,直到该值变为大于 0 时再将它减 1 后才返回。函数成功返回 0,否则返回-1;参数 sem 为需要操作的信号量。
V操作
信号量的 V 操作有 sem_post()函数来完成,它的函数原型如下:
#include <semaphore.h>
int sem_post(sem_t *sem);
当一个进程(线程) 使用完某个信号量时,它应该调用 sem_post()来告诉系统申请的资源已经使用完毕。 sem_post()函数与 sem_wait()函数的功能正好相反,它把所指定的信号量的值加 1,然后唤醒正在等待该信号量的任意进程(线程) 。
函数成功返回 0,否则返回-1;
参数 sem 为需要操作的信号量。
销毁基于内存的信号量
销毁基于内存的信号量可使用 sem_destroy()函数来完成,
#include <semaphore.h>
int sem_destroy(sem_t *sem);
该函数只能销毁由 sem_init()初始化的信号量。销毁后该信号量将不能再被使用。函数成功返回 0,否则返回-1。
参数 sem 指出需要销毁的信号量。
删除有名信号量
当相关的进程都已完成对有名信号量的使用时,可以用sem_unlink()函数用来删除它,以释放资源。
#include <semaphore.h>
int sem_unlink(const char *name);
范例:
注意编译的时候需要增加两个参数-pthread 和 -lrt
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>
#include <errno.h>
#define MAPSIZE 100 /* 共享内存大小, 100 字节 */
int main(int argc, char **argv)
{
int shmid;
char *ptr;
sem_t *semid;
if (argc != 2) { /* 参数 argv[1]指定共享内存和信号量的名字 */
printf("usage: %s <pathname>\n", argv[0]);
return -1;
}
shmid = shm_open(argv[1], O_RDWR|O_CREAT, 0644); /* 创建共享内存对象 */
if (shmid == -1) {
printf( "open shared memory error\n");
return -1;
}
ftruncate(shmid, MAPSIZE); /* 设置共享内存大小 */
/* 将共享内存进行映射 */
ptr = mmap(NULL, MAPSIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shmid, 0);
strcpy(ptr,"\0");
semid = sem_open(argv[1], O_CREAT, 0644, 0); /* 创建信号量对象 */
if (semid == SEM_FAILED) {
printf("open semaphore error\n");
return -1;
}
sem_wait(semid); /* 信号量等待操作,等待客户端修改共享内存 */
printf("server recv:%s",ptr); /* 从共享内存中读取值 */
strcpy(ptr,"\0");
munmap(ptr, MAPSIZE); /* 取消对共享内存的映射 */
close(shmid); /* 关闭共享内存 */
sem_close(semid); /* 关闭信号量 */
sem_unlink(argv[1]); /* 删除信号量对象 */
shm_unlink(argv[1]); /* 删除共享内存对象 */
return 0;
}
程序从启动参数获取共享内存名称, 然后创建共享内存对象,设置大小后完成映射; 最后创建信号量对象并等待客户端的通知,其中信号量的初始值为 0。在共享内存映射后使用 sem_wait()函数等待客户端完成对共享内存的写入操作。
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>
#include <errno.h>
#define MAPSIZE 100 /* 共享内存大小, 100 字节 */
int main(int argc, char **argv)
{
int shmid;
char *ptr;
sem_t *semid;
if (argc != 2) {
printf("usage: %s <pathname>\n", argv[0]); /* 参数 argv[1]指定共享内存和信号量的名字 */
return -1;
}
shmid = shm_open(argv[1], O_RDWR, 0); /* 打开共享内存对象 */
if (shmid == -1) {
printf( "open shared memory error.\n");
return -1;
}
ftruncate(shmid, MAPSIZE); /* 设置共享内存大小 */
/* 将共享内存进行映射 */
ptr = mmap(NULL, MAPSIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shmid, 0);
semid = sem_open(argv[1], 0); /* 打开信号量对象 */
if (semid == SEM_FAILED) {
printf("open semaphore error\n");
return -1;
}
printf("client input:");
fgets(ptr, MAPSIZE, stdin); /* 从标准输入读取需要写入共享内存的值 */
sem_post(semid); /* 通知服务器端 */
munmap(ptr, MAPSIZE); /* 取消对共享内存的映射 */
close(shmid);
sem_close(semid);
return 0;
}
程序从启动参数获取共享内存名称, 然后创建共享内存对象,设置大小后完成映射; 最后创建信号量对象并等待客户端的通知,其中信号量的初始值为 0。在共享内存映射后使用sem_wait()函数等待客户端完成对共享内存的写入操作。
服务端和客户端携带相同的参数运行,客户端输入数据并按回车后服务端将获取到数据,完成通信后两个进程退出。
消息队列
消息队列是消息的链接表 ,存放在内核中并由消息队列标识符标识。我们将称消息队列为“队列”,其标识符为“队列 I D”。
函数名 | 作用 |
---|---|
ftok | 生成一个key值 |
msgget | 用于创建一个新队列或打开一个现存的队列 |
msgsnd | 用于将新消息添加到队列尾端。每个消息包含一个正长整型类型字段,一个非负长度以及实际数据字节(对应于长度),所有这些都在将消息添加到队列时,传送给 msgsnd |
msgrcv | 用于从队列中取消息。我们并不一定要以先进先出次序取消息,也可以按消息的类型字段取消息 |
- ftok函数
#include <sys/types.h>
#include <sys/ipc.h>
key_t requsetKey;
/*根据不同的路径和关键表示产生标准的key*/
if (-1 == (requsetKey = ftok("/etc/custom/coap-client", 1)))
{
printf("ftok1 error");
return VOS_ERR;
}
- 函数原型:key_t ftok(const char *pathname, int proj_id);
- 作用:系统建立IPC通讯(如消息队列、共享内存、信号量)必须指定一个key值,来作为唯一得标识。
- 函数说明:key_t ftok(const char *pathname, int proj_id);
pathname是指定的文件名,这个文件必须是存在的而且可以访问的。
proj_id:子序号,它是一个8bit的整数。即范围是0~255。同一个路径下编写代码,防止大家使用了相同得key值。
关于ftok()函数的一个陷阱
在使用ftok()函数时,里面有两个参数,即fname和id,fname为指定的文件名,而id为子序列号,这个函数的返回值就是key,它与指定的文件的索引节点号和子序列号id有关,这样就会给我们一个误解,即只要文件的路径,名称和子序列号不变,那么得到的key值永远就不会变。
事实上,这种认识是错误的,想想一下,假如存在这样一种情况:在访问同一共享内存的多个进程先后调用ftok()时间段中,如果fname指向的文件或者目录被删除而且又重新创建,那么文件系统会赋予这个同名文件新的i节点信息,于是这些进程调用的ftok()都能正常返回,但键值key却不一定相同了。由此可能造成的后果是,原本这些进程意图访问一个相同的共享内存对象,然而由于它们各自得到的键值不同,实际上进程指向的共享内存不再一致;如果这些共享内存都得到创建,则在整个应用运行的过程中表面上不会报出任何错误,然而通过一个共享内存对象进行数据传输的目 的将无法实现。
所以要确保key值不变,要么确保ftok()的文件不被删除,要么不用ftok(),指定一个固定的key值。
- msgget函数
应用实例
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
/* 建立采集请求消息队列 */
requestQueId = msgget(requsetKey, 0666 | IPC_CREAT);
if (requestQueId == -1)
{
printf("\nCreat datarequest queue failed\n");
return VOS_ERR;
}
-
函数原型 int msgget(key_t key, int flag|mode);
-
功能:创建或取得一个消息队列对象
-
返回:消息队列对象的id 同一个key得到同一个对象
-
格式:msgget(key,flag|mode);
-
flag:可以是0或者IPC_CREAT(不存在就创建)
-
mode:同文件权限一样
-
msgsnd函数
应用实例
/* 向采集请求队列发送数据 */
if (0 != msgsnd(requestQueId, (void *)&sendMsg, sizeof(message_s), 0))
{
printf("\nmsgsnd failed.\n");
return VOS_ERR;
}
-
函数原型
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); -
功能:将msgp消息写入标识为msgid的消息队列
msgp:
struct msgbuf {
long mtype; /* message type, must be > 0 /消息的类型必须>0
char mtext[1]; / message data */长度随意
}; -
msgsz:要发送的消息的大小 不包括消息的类型占用的4个字节
-
msgflg: 如果是0 当消息队列为满 msgsnd会阻塞,如果是IPC_NOWAIT 当消息队列为满时 不阻塞 立即返回
-
返回值:成功返回id 失败返回-1
-
msgrcv函数
应用实例
/* IPC_NOWAIT用于设置非阻塞读取消息队列 */
if (0 < msgrcv(requestQueId, (void *)&revMsg, sizeof(message_s), msg_type, IPC_NOWAIT))
{
dataResult = revMsg.data_info;
-
函数原型
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg); -
功能:从标识符为msgid的消息队列里接收一个指定类型的消息 并 存储于msgp中 读取后 把消息从消息队列中删除
-
msgtyp:为 0 表示无论什么类型 都可以接收,不为零表示指定类型收。
-
msgp:存放消息的结构体
-
msgsz:要接收的消息的大小 不包含消息类型占用的4字节
-
msgflg:如果是0 标识如果没有指定类型的消息 就一直等待,如果是IPC_NOWAIT 则表示不等待
-
msgctl函数
实例应用
/* 删除消息队列 */
if (msgctl(requestQueId, IPC_RMID, 0) == -1)
{
printf("msgctl(IPC_RMID)1 failed\n");
}
-
函数原型:
int msgctl(int msqid, int cmd, struct msqid_ds *buf); -
msgctl:系统调用对msgqid标识的消息队列执行cmd操作,系统定义了3种cmd操作:
-
IPC_STAT:该命令用来获取消息队列对应的msqid_ds数据结构,并将其保存到buf指向的地址空间
-
IPC_SET:该命令用来设置消息队列的属性,要设置的属性存储在buf中,可设置的属性包括:msg_perm.uid 、 msg_perm.gid、msg_perm.mode以及msg_qbytes
-
IPC_RMID:从内核中删除msgqid标识的消息队列
-
源码send
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
typedef struct{
long type;
char name[20];
int age;
}Msg;
int main()
{
key_t key = ftok("/home",'6');
printf("key:%x\n",key);
int msgid = msgget(key,IPC_CREAT|0777);
if(msgid<0)
{
perror("msgget error!");
exit(-1);
}
Msg m;
puts("please input your type name age:");
scanf("%ld%s%d",&m.type,m.name,&m.age);
msgsnd(msgid,&m,sizeof(m)-sizeof(m.type),0);
return 0;
}
- 源码rev
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
typedef struct{
long type;
char name[20];
int age;
}Msg;
int main()
{
key_t key = ftok("/home",'6');
printf("key:%x\n",key);
int msgid = msgget(key,0);
if(msgid<0)
{
perror("msgget error!");
exit(-1);
}
Msg rcv;
long type;
puts("please input type you want!");
scanf("%ld",&type);
msgrcv(msgid,&rcv,sizeof(rcv)-sizeof(type),type,0);
printf("rcv--name:%s age:%d\n",rcv.name,rcv.age);
msgctl(msgid,IPC_RMID,0);
return 0;
}