Most perlin noise algorithms will allow you to retrieve the noise value at any given location, with something like
noise(x,y,z). This makes it fairly trivial to generate noise on a chunk by chunk basis. All you need to do is pass the global position, instead of the chunk position.
for(int i = 0; i < CHUNKMAX_X; i++)
for(int j = 0; j < CHUNKMAX_Y; j++)
for(int k = 0; k < CHUNKMAX_Z; k++)
if(isSolid(perlinNoise.get(chunkPosition.x + i,
chunkPosition.y + j,
chunkPosition.z + k))
thisChunk[i,j,k] = new Voxel(solid);
thisChunk[i,j,k] = new Voxel(air);
So you can see, we're generating terrain for the chunk, by iterating over the chunk bounds, and checking to see if that global position is solid or not. This is likely the same methodology you're using to generate the terrain in general.
perlinNoise.get takes a global position and returns its density. Where
isSolid would just be a simple test to see if the voxel is "dense enough" to qualify for solid.
perlinNoise.get can be more complex than just a simple noise algorithm. You can have checks based on the depth of the voxel in your world. For example, if the voxel is below what you've decided is "absolute base ground level" then it can use the perlin worms algorithm to return a density, if it's above the absolute base, it can use a normal density function to give you more varied terrain. I would recommend some blending between the two however.
Combining different Perlin noise functions is just something you have to play with and see what works. It's best to set up your environment so that you can just change some values and hot-swap the terrain without needing to reload your game. Happy experimenting.