每个进程在初始化的时候系统都分配了一个ID号,在LInux中进程号是唯一的,描述进程的ID号通常叫做PID(process ID),PID的变量类型为pid_t。
getpid函数返回当前进程ID。getppid返回当前进程的父进程的ID。类型pid_t其实是一个typedef类型,定义为unsigned int。
头文件 sys/types.h unistd.h
函数原型
pid_t getpid(void);
pid_t getppid(void)
头文件 sys/types.h unistd.h
函数原型
pid_t fork(void);
fork函数的返回值是进程的ID;失败返回-1
fork的特点是执行一次,返回两次。在父进程和子进程中的返回值是不一样的,父进程中返回的是子进程的ID号,而子进程中则返回0。
#include
#include
#include
int main(int argc, char *argv[]) {
pid_t pid, ppid;
pid = fork();
if(pid == -1) {
puts("产生进程失败");
}
else {
if(pid == 0) {
puts("这是子进程");
printf("子进程fork返回值 : %u\n",pid);
pid = getpid();
ppid = getppid();
printf("子进程的ID为 %u\n",pid);
printf("子进程父进程的ID为 %u\n",ppid);
}
else {
puts("这是父进程");
printf("父进程fork返回值 : %u\n",pid);
pid = getpid();
ppid = getppid();
printf("父进程的ID为 %u\n",pid);
printf("父进程的父进程的ID为 %u\n",ppid);
}
}
}
发现子进程的父进程的ID有时候是产生子进程的那个进程的ID,有时候却是 1。
system函数调用shell的外部命令在当前进程中开始另一个进程(这个函数是C语言中的函数)
头文件stdlib.h
函数原型
int system(const char *command);
system函数调用“/bin/sh-c command”执行特定的命令,阻塞当前进程直到command执行完毕。
执行system函数的时候会调用fork、execve、waitpid等函数,其中任意一个调用失败将导致system函数调用失败。system函数的返回值如下:
在使用fork和system函数的时候,系统中都会建立一个新的进程,执行调用者的操作,而原来的进程还会存在,直到用户显示的退出。而exec族的函数与之前的fork和system函数不一样,exec函数会用新进程代替原有的进程,系统会从新进程开始运行,新进程的PID与原进程的PID相同。
头文件 unistd.h
函数原型
extern char **environ;
int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
上述六个函数在linux man手册中都是属于(3)类型的,也就是库函数,其实他们都是对execve这个系统函数的封装。
头文件 unistd.h
函数原型
int execve(const char *filename, char *const argv[], char *const envp[]);
上述exec函数族的作用是:在当前系统中根据指定的文件路径寻找可执行文件并用它来取代调用进程的内容。即在原来的进程内部执行一个可执行文件,这个文件既可以是二进制文件,也可以是可执行的脚本文件。
与fork函数不同,exec函数执行成功后不会返回,这是因为当前进程的空间和资源已经被占用了,这些资源包括代码段、数据段和堆栈等。它们都已经被新内容取代,而进程的ID等标识性的东西任然是原来的东西,即exec函数在原来进程的壳上运行了自己的东西。只有程序运行失败了系统才会返回-1。
使用exec函数的一种普遍的方法是先使用fork函数分叉进程,然后在新的进程中调用exec函数。linux系统针对上述过程专门进行了优化。由于fork的过程是对原有系统进行复制,然后建立子进程,这些过程都比较耗费时间。如果在fork系统调用之后进程exec系统调用,系统就不会进行系统复制,而是直接使用exec指定的参数来覆盖原有的进程。上述的方法在linux系统上叫做写时复制(copy ont write)
,即只有在造成系统的内容发生更改的时候才进行进程的真正更新。
#include
#include
#include
int main(int argc, char *argv[]) {
pid_t pid ;
pid = getpid();
printf("进程的PID为 %d\n",pid);
if(execve("/bin/ls",argv, NULL) < 0) {
puts("创建进程出错");
}
puts("这里还会执行吗?");
}
在linux系统中所有进程都有父子或者堂兄第关系的,除了初始进程init,没有那个进程与其他进程完全独立。系统中每个进程都有一个父进程,新的进程不是被全新的创建,通常是从一个原有的进程进行复制或者克隆的。可以使用命令pstree查看系统中运行的进程之间的关系。我发现我的最初始的进程是systemd而不是init
网上搜了一下,systemd取代了init。参考资料
在linux下多个进程之间的通信机制叫做IPC,它是多个进程之间相互沟通的一种方法。在Linux下有多种进程间的通信方法:半双工管道、FIFO(命名管道)、消息队列、信号量、共享内存等。使用这些通信机制可以为linux下的网络服务器开发提供灵活而又坚固的框架。
管道是一种把两个进程之间的标准输入和标准输出连接起来的机制。管道是一种历史悠久的进程间通信的方法,自unix操作系统诞生,管道就存在了。
由于管道仅仅是将某个进程的输出与另一个进程的输入相连接的单向通信的方法,因此称其为半双工。在shell中管道用 |表示,例如ls -l | grep *.c表示将ls -l的输出作为grep *.c的输入。管道在前一个进程中建立输入通道,在后一个进程中建立输出通道。
进程创建管道每次创建两个文件描述符来操作管道,其中一个作为输入,另一个作为输出。

把管道想象成一个文件。对管道的读写与一般的IO系统函数一致,使用write函数写入数据,read函数读出数据,某些特定的IO操作管道是不支持的,例如偏移函数lseek。
创建管道的函数原型为
#include
int pipe(int pipefd[2]);
pipefd是一个文件描述符的数组,用于返回管道保存的两个文件描述符,数组中第一个元素(下标为0)是为了读操作而创建和打开的,第二个元素是为了写元素而创建和打开的。pipe函数执行成功时返回0,失败时返回-1。
只建立管道似乎没什么用,要使管道有切实的用处,需要与进程的创建结合起来,利用两个管道在父进程和子进程之间进行通信。

要实现这样的模型,在父进程中需要关闭写端,在子进程中需要关闭读端。
为了方便阅读,创建两个指针write_fd , read_fd分别指向fd[1]和fd[0]。
#include
#include
#include
#include
#include
int main(int argc, char *argv[]) {
int result = -1;//存储创建管道的结果
int fd[2];
pid_t pid;
int *write_fd = &fd[1];
int *read_fd = &fd[0];
result = pipe(fd);
if(result == -1) {
puts("管道创建失败");
return -1;
}
pid = fork();//分叉程序
if(pid == -1) {
puts("进程fork失败");
return -1;
}
if(pid == 0) {//子进程
close(*read_fd);//关闭读
//向管道写入数据
write(*write_fd, "你好,管道", strlen("你好,管道"));
return 0;
}
else {
close(*write_fd);//关闭写
//从管道读取数据
char *buf = (char*)malloc(100);
memset(buf, 0, 100);
read(*read_fd, buf, 100);
printf("从管道收到的数据为 :%s\n",buf);
}
return 0;
}
试试不在父进程中关闭写 : 还是能正常接收到数据
试试不在子进程中关闭读:还是能正常接收到数据
试试同时不关闭读和写:仍然能接收到数据
看来这个关闭只是为了模型上的匹配,对实际没有影响。
#include
#include
#include
#include
#include
int main(int argc, char *argv[]) {
int result = -1;//存储创建管道的结果
int fd[2];
pid_t pid;
int *write_fd = &fd[1];
int *read_fd = &fd[0];
result = pipe(fd);
if(result == -1) {
puts("管道创建失败");
return -1;
}
pid = fork();//分叉程序
if(pid == -1) {
puts("进程fork失败");
return -1;
}
if(pid == 0) {//子进程
close(*read_fd);//关闭读
//向管道写入数据
char buf[100];
puts("输出你想传送到父进程的消息:");
while(scanf("%s",buf) != 0) {
write(*write_fd, buf, strlen(buf));
puts("输出你想传送到父进程的消息:");
}
return 0;
}
else {
close(*write_fd);//关闭写
//从管道读取数据
char *buf = (char*)malloc(100);
memset(buf, 0, 100);
while(1) {
read(*read_fd, buf, 100);
printf("从管道中接收到的消息为: %s\n",buf);
}
}
return 0;
}
有多种方式可以创建命名管道,例如使用shell直接创建:mkfifo namedfifo
ls -l 可以看到文件属性为prw-rw-r-- 1 luoxin luoxin 0 8月 20 09:19 namedfifo前面的p代表这是一个pipe(管道)
或者使用C语言创建,需要用到mkfifo函数
#include
#include
int mkfifo(const char *pathname, mode_t mode);
对于FIFO来说,IO操作与普通管道的IO操作基本上是一致的,两者之间存在一个主要的区别:在FIFO中必须使用一个open函数来显式的建立连接到管道的通道。一般来说FIFO总是处于阻塞状态,也就是说如果FIFO打开时设置了读权限,则读进程将一致阻塞,直到其他进程打开该FIFO并且向管道中写入数据。这个阻塞动作反过来也是成立的,如果一个进程打开管道写入数据,当没有进程从管道中读取数据的时候,写操作的管道也是阻塞的,直到已经写入的数据被读取后,才能进行写入操作。如果不希望在进行命名管道操作的时候发生阻塞,可以在open调用中使用O_NONBLOCK标志,以关闭默认的阻塞动作。
//下面的代码验证读进程会因为写进程而阻塞
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[]) {
pid_t pid = fork();
if(pid == -1) {
puts("进程分叉失败");
return -1;
}
if(pid == 0) {//子进程
//打开管道并进行写操作
char str[] = "hello";
int write_fd = open("/tmp/ipc/namedfifo", O_WRONLY);
puts("before write");
sleep(2);//休眠两秒,以验证读进程会阻塞
write(write_fd, str, strlen(str));
puts("after write");
close(write_fd);
}
else {//父进程
//打开管道并读取
char buf[100];
int read_fd = open("/tmp/ipc/namedfifo", O_RDONLY);
memset(buf, 0, 100);
read(read_fd, buf,100);
printf("从管道中获取到的数据为: %s\n",buf);
close(read_fd);
}
return 0;
}
//下面的代码验证写进程会因为没有读进程读取数据而阻塞但是失败了,不知道是我理解错了书上的意思还是说系统有所改动。
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[]) {
pid_t pid = fork();
if(pid == -1) {
puts("进程分叉失败");
return -1;
}
if(pid == 0) {//子进程
//打开管道并进行写操作
char str[] = "hello";
int write_fd = open("/tmp/ipc/namedfifo", O_WRONLY);
puts("before write1");
//sleep(2);//休眠两秒,以验证读进程会阻塞
write(write_fd, str, strlen(str));
puts("after write1");
puts("before write2");
write(write_fd, str, strlen(str));
puts("after write2");
close(write_fd);
}
else {//父进程
//打开管道并读取
char buf[100];
int read_fd = open("/tmp/ipc/namedfifo", O_RDONLY);
memset(buf, 0, 100);
sleep(2);//休眠2秒,已以验证写进程会阻塞
read(read_fd, buf,100);
printf("从管道中获取到的数据为: %s\n",buf);
close(read_fd);
}
return 0;
}
消息队列是内核地址空间中的内部链表,通过linux内核在各个进程之间传递内容。消息顺序的发送到消息队列中,并以几种不同的方式从队列中获取,每个消息队列可以用IPC标识符唯一的标识。内核中的消息队列是通过IPC标识符来区分的,不同的消息队列之间是相互独立的,每个消息队列中的消息,又构成一个独立的链表。
常用的结构是msgbuf结构。程序员可以以这个结构为模板定义自己的消息结构,在头文件linux/msg.h中,它的定义如下:
在/usr/include/linux/msg.h中查看
/* message buffer for msgsnd and msgrcv calls */
struct msgbuf {
__kernel_long_t mtype; /* type of message */
char mtext[1]; /* message text */
};
mtype:消息类型,设置消息类型以区分不同进程之间的消息
mtext:消息内容,可以重写自己的消息扩大消息内容的长度,例如:
struct my_msgbuf {
__kernel_long_t mtype; /* type of message */
char mtext[10]; /* message text */
int length;
};
每个消息队列都维护了一个这样的结构。
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg *msg_first; /* first message on queue,unused */
struct msg *msg_last; /* last message in queue,unused */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* last change time */
unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
unsigned long msg_lqbytes; /* ditto */
unsigned short msg_cbytes; /* current number of bytes on queue */
unsigned short msg_qnum; /* number of messages in queue */
unsigned short msg_qbytes; /* max number of bytes on queue */
__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
};
其中struct ipc_perm定义在/usr/include/linux/ipc.h
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct ipc_perm
{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
#include
#include
key_t ftok(const char *pathname, int proj_id);
其中pathname必须是已经存在的目录,而proj_id是一个8位的值。这个生产的key在msgget函数中需要,用于获取或者创建一个消息队列。
#include
#include
#include
int msgget(key_t key, int msgflg);
从内核中获取与key相同的队列,比较后打开或者访问操作依赖于msgflg参数的内容。
返回一个消息队列标识符。这个消息队列标识符在后面的发送消息和接受消息的函数中会使用到。
#include
#include
#include
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
第一个参数是消息队列标识符,第二个参数是一个void型指针,指向一个消息缓冲区,用于发送或者接收消息。第三个参数是一个size型参数,对于发送来说就是发送的消息的size,对于接收来说就是期望接收的消息的大小。msgflag是标志参数。对于msgsnd来说:
对于msgrcv来说:
如果在等待消息时队列被删除了,则返回EIDRM,
其中msgrcv函数中的msgtyp参数用于指定消息的类型,这样才能达到精准通信的目的。
msgctl可以直接对消息队列的内部结构进程控制。
#include
#include
#include
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
第一个参数是队列标识符,指明要操控的队列,第三个参数是 msqid_ds结构体,用于设置或者接收队列的内部结构信息。第二个参数cmd可以为以下值:
信号量是一种计数器,用来控制对多个进程共享的资源所进行的访问。它们常常被用来做一个锁机制。生产者和消费者模型是信号量的典型使用。
union semun {
int val;//整性变量
struct semid_ds *buf;//semid_ds型结构指针
unsigned short *array;//数组类型
struct seminfo *__buf;//信号量内部结构
}
semget函数用于创建一个新的信号量集合或者访问现有的集合。
#include
#include
#include
int semget(key_t key, int nsems, int semflg);
第一个参数是ftok生成的键值,第二个参数可以指定在新的集合中应该创建的信号量的数目。
大概的意思就是第二个参数可以为0当信号量已经存在的时候,但是如果要新建信号量,则第二个参数必须是一个大于0 小于等于最大值的数。
The argument nsems can be 0 (a don’t care) when a semaphore set is not being created. Otherwise, nsems must be greater than 0 and less than or equal to the maximum number of semaphores per semaphore set (SEMMSL).
第三个参数是打开信号量的方式。可以是
这两个参数前面已经讲了很多遍了,这里不再赘述。
返回值是一个信号量描述符。
下面封装的函数实现获取指定键值的信号量并设置其值为value。
typedef int sem_t;
union semun {
int val;//整性变量
struct semid_ds *buf;//semid_ds型结构指针
unsigned short *array;//数组类型
}arg;
sem_t createSem(key_t key, int value) {
union semun sem;
sem_t sem_id;
sem.val = value;
sem_id = semget(key, 0, IPC_CREAT | IPC_EXCL);
if(sem_id == -1) {
puts("创建信号量失败");
return -1;
}
semctl(sem_id, 0, SETVAL, sem);
return sem_id;
}
信号量的P、V操作是通过向已经建立好的信号量发送命令来完成的。向信号量发送命令的函数是semop。函数原型如下:
#include
#include
#include
int semop(int semid, struct sembuf *sops, size_t nsops);
int semtimedop(int semid, struct sembuf *sops, size_t nsops, const struct timespec *timeout);
第一个参数是信号量标识符,第二个参数是一个指向 需要在该信号量上执行的操作的数组 的指针。第三个参数是该数组中操作的个数,其实也就是参数二数组的长度。
sembuf 的结构如下:
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
#include
#include
#include
int semctl(int semid, int semnum, int cmd, ...);
cmd取值范围为:
可以在第四个参数的时候传入一个semun联合体
共享内存是通过在多个进程之间对内存段进行映射的方式实现内存共享的。这是IPC最快捷的方式。
#include
#include
int shmget(key_t key, size_t size, int shmflg);
shmflg:
返回一个共享内存的描述符
#include
#include
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
如果shmaddr等于0 ,则内核将尝试找一个未映射的区域。用户可以指定一个地址,但通常该地址只用于访问所拥有的硬件,或者解决与其他应用程序的冲突。SHM_RND标志可以与标志参数进行OR操作,这样可以让传递的地址页对齐。SHM_RDONLY打开的共享内存只读。
#include
#include
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid_ds:
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
信号是unix最为古老的进程之间进行通信的机制。它用于在一个或者多个进程之间传递异步信号。
linux定义了一系列的信号,这些信号可以由内核产生,也可以由系统中其他进程产生,只要这些进程有足够的权限。可以使用kill -l
列出机器上所有的信号
luoxin@luoxin-virtual-machine:~$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
进程可以屏蔽掉大多数的信号,除了SIGSTOP 和SIGKILL,一个是暂停信号,一个是退出信号。
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
luoxin@luoxin-virtual-machine:~$ man -f kill
kill (1) - send a signal to a process
kill (2) - send signal to a process
luoxin@luoxin-virtual-machine:~$ man -f raise
raise (3) - send a signal to the caller
#include
#include
//向某个进程发送信号,如果pid为 0 时向所有进程发送信号
int kill(pid_t pid, int sig);
#include
//向当前进程发送信号
int raise(int sig);
) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
进程可以屏蔽掉大多数的信号,除了SIGSTOP 和SIGKILL,一个是暂停信号,一个是退出信号。
#### 信号截取函数signal
```c
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
luoxin@luoxin-virtual-machine:~$ man -f kill
kill (1) - send a signal to a process
kill (2) - send signal to a process
luoxin@luoxin-virtual-machine:~$ man -f raise
raise (3) - send a signal to the caller
#include
#include
//向某个进程发送信号,如果pid为 0 时向所有进程发送信号
int kill(pid_t pid, int sig);
#include
//向当前进程发送信号
int raise(int sig);